REST API Testing: Postman, curl, and Automated Tests
Learn to test REST APIs manually with Postman and curl, then automate with Jest and Supertest. Covers status codes, response validation, and CI integration.
What you'll learn
- ✓Testing APIs with curl from the terminal
- ✓Using Postman for manual exploration
- ✓Writing automated API tests with Jest
- ✓Integrating API tests into CI pipelines
Prerequisites
- •Basic REST API concepts
- •Familiarity with HTTP methods
Why Test Your API
APIs are contracts. Your clients depend on specific endpoints returning specific shapes of data with specific status codes. Without tests, every deployment is a gamble. You might break a response format, return wrong status codes, or introduce regressions that only surface when a customer complains.
API testing catches these problems before they reach production.
Testing with curl
curl is available on every operating system and requires no setup. It is the fastest way to poke at an API.
Basic Requests
# GET request
curl https://jsonplaceholder.typicode.com/posts/1
# Pretty-print JSON (pipe through jq)
curl -s https://jsonplaceholder.typicode.com/posts/1 | jq .
# See response headers
curl -i https://jsonplaceholder.typicode.com/posts/1
# Only see headers (HEAD request)
curl -I https://jsonplaceholder.typicode.com/posts/1
POST, PUT, and DELETE
# POST with JSON body
curl -X POST https://jsonplaceholder.typicode.com/posts \
-H "Content-Type: application/json" \
-d '{"title": "New Post", "body": "Content here", "userId": 1}'
# PUT to update
curl -X PUT https://jsonplaceholder.typicode.com/posts/1 \
-H "Content-Type: application/json" \
-d '{"id": 1, "title": "Updated Title", "body": "New body", "userId": 1}'
# PATCH for partial update
curl -X PATCH https://jsonplaceholder.typicode.com/posts/1 \
-H "Content-Type: application/json" \
-d '{"title": "Just the title changed"}'
# DELETE
curl -X DELETE https://jsonplaceholder.typicode.com/posts/1
Authentication
# Bearer token
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..." \
https://api.example.com/v1/users
# API key
curl -H "X-API-Key: sk_live_abc123" \
https://api.example.com/v1/data
# Basic auth
curl -u username:password https://api.example.com/v1/data
Useful curl Flags
# Write HTTP status code to stdout
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/health
# Set a timeout (5 seconds)
curl --max-time 5 https://api.example.com/slow-endpoint
# Follow redirects
curl -L https://api.example.com/old-endpoint
# Verbose output (see full request/response)
curl -v https://api.example.com/v1/posts
Testing with Postman
Postman provides a GUI for building, sending, and organizing API requests. It is especially useful for exploring unfamiliar APIs and sharing request collections with your team.
Setting Up a Collection
- Create a new collection called “My API.”
- Add folders for each resource: Users, Posts, Comments.
- Add requests to each folder.
- Use environment variables for base URL and tokens.
Environment Variables
Set up variables so you can switch between development and production:
Variable: base_url
Dev value: http://localhost:3000/api
Prod value: https://api.example.com
Variable: auth_token
Value: eyJhbGciOiJIUzI1NiJ9...
Use them in requests as {{base_url}}/users and in headers as Bearer {{auth_token}}.
Writing Postman Tests
Postman has a built-in test runner. Add tests in the “Scripts > Post-response” tab:
// Check status code
pm.test("Status is 200", function () {
pm.response.to.have.status(200);
});
// Check response time
pm.test("Response time is under 500ms", function () {
pm.expect(pm.response.responseTime).to.be.below(500);
});
// Validate JSON structure
pm.test("Response has correct shape", function () {
const json = pm.response.json();
pm.expect(json).to.have.property("id");
pm.expect(json).to.have.property("title");
pm.expect(json).to.have.property("body");
pm.expect(json.id).to.be.a("number");
});
// Check headers
pm.test("Content-Type is JSON", function () {
pm.response.to.have.header("Content-Type", "application/json; charset=utf-8");
});
// Save a value for later requests
pm.test("Save user ID", function () {
const json = pm.response.json();
pm.environment.set("userId", json.id);
});
Running Collections
Use the Collection Runner to execute all requests in sequence. This is great for testing workflows like “create user, then create post by that user, then fetch the post.”
For CI/CD, export the collection and run it with Newman:
npm install -g newman
newman run my-api-collection.json \
--environment dev-environment.json \
--reporters cli,junit \
--reporter-junit-export results.xml
Automated Testing with Jest and Supertest
Manual testing does not scale. Automated tests run on every commit and catch regressions instantly.
Setup
npm install --save-dev jest supertest
Basic Test Structure
// tests/api/users.test.js
const request = require('supertest');
const app = require('../../src/app');
describe('GET /api/users', () => {
it('returns a list of users', async () => {
const res = await request(app)
.get('/api/users')
.expect('Content-Type', /json/)
.expect(200);
expect(res.body).toBeInstanceOf(Array);
expect(res.body.length).toBeGreaterThan(0);
expect(res.body[0]).toHaveProperty('id');
expect(res.body[0]).toHaveProperty('email');
});
it('supports pagination', async () => {
const res = await request(app)
.get('/api/users?page=1&limit=5')
.expect(200);
expect(res.body.length).toBeLessThanOrEqual(5);
});
});
Testing CRUD Operations
describe('Users CRUD', () => {
let createdUserId;
it('POST /api/users creates a user', async () => {
const res = await request(app)
.post('/api/users')
.send({
name: 'Jane Doe',
email: 'jane@example.com',
role: 'editor',
})
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.name).toBe('Jane Doe');
expect(res.body.email).toBe('jane@example.com');
createdUserId = res.body.id;
});
it('GET /api/users/:id returns the created user', async () => {
const res = await request(app)
.get(`/api/users/${createdUserId}`)
.expect(200);
expect(res.body.id).toBe(createdUserId);
expect(res.body.name).toBe('Jane Doe');
});
it('PUT /api/users/:id updates the user', async () => {
const res = await request(app)
.put(`/api/users/${createdUserId}`)
.send({ name: 'Jane Smith', email: 'jane@example.com', role: 'editor' })
.expect(200);
expect(res.body.name).toBe('Jane Smith');
});
it('DELETE /api/users/:id removes the user', async () => {
await request(app)
.delete(`/api/users/${createdUserId}`)
.expect(204);
await request(app)
.get(`/api/users/${createdUserId}`)
.expect(404);
});
});
Testing Error Responses
describe('Error handling', () => {
it('returns 400 for invalid input', async () => {
const res = await request(app)
.post('/api/users')
.send({ name: '' }) // Missing required fields
.expect(400);
expect(res.body).toHaveProperty('error');
expect(res.body.error).toMatch(/email.*required/i);
});
it('returns 404 for non-existent resource', async () => {
const res = await request(app)
.get('/api/users/99999')
.expect(404);
expect(res.body.error).toBe('User not found');
});
it('returns 401 without authentication', async () => {
await request(app)
.get('/api/admin/settings')
.expect(401);
});
it('returns 403 with insufficient permissions', async () => {
const res = await request(app)
.get('/api/admin/settings')
.set('Authorization', `Bearer ${editorToken}`)
.expect(403);
expect(res.body.error).toMatch(/forbidden/i);
});
});
Testing with Authentication
describe('Authenticated endpoints', () => {
let token;
beforeAll(async () => {
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'admin@example.com', password: 'testpass123' });
token = res.body.accessToken;
});
it('returns user profile with valid token', async () => {
const res = await request(app)
.get('/api/me/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(res.body).toHaveProperty('email', 'admin@example.com');
});
});
Testing Response Schemas
Use a JSON schema validator to ensure your API always returns the correct shape:
const Ajv = require('ajv');
const ajv = new Ajv();
const userSchema = {
type: 'object',
required: ['id', 'name', 'email', 'createdAt'],
properties: {
id: { type: 'number' },
name: { type: 'string', minLength: 1 },
email: { type: 'string', format: 'email' },
createdAt: { type: 'string' },
},
additionalProperties: false,
};
const validateUser = ajv.compile(userSchema);
it('matches the user schema', async () => {
const res = await request(app).get('/api/users/1').expect(200);
const valid = validateUser(res.body);
if (!valid) {
console.log(validateUser.errors);
}
expect(valid).toBe(true);
});
CI Pipeline Integration
Add API tests to your CI pipeline so they run on every pull request:
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: testdb
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run db:migrate
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/testdb
- run: npm test -- --coverage
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/testdb
JWT_SECRET: test-secret-do-not-use-in-prod
Test Organization Tips
Structure your tests to mirror your routes:
tests/
api/
auth.test.js
users.test.js
posts.test.js
comments.test.js
helpers/
auth.js # Login helpers, token generation
fixtures.js # Test data factories
setup.js # Database setup/teardown
Create helper functions to reduce duplication:
// tests/helpers/auth.js
async function loginAsAdmin(app) {
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'admin@test.com', password: 'password123' });
return res.body.accessToken;
}
async function loginAsUser(app) {
const res = await request(app)
.post('/api/auth/login')
.send({ email: 'user@test.com', password: 'password123' });
return res.body.accessToken;
}
module.exports = { loginAsAdmin, loginAsUser };
Wrapping Up
Start with curl for quick manual checks, use Postman for exploring APIs and sharing collections with your team, then build automated tests with Jest and Supertest for your CI pipeline. Test the happy path, error cases, authentication, and response schemas. Automated API tests are some of the highest-value tests you can write because they verify actual HTTP behavior end to end with minimal mocking.
Related articles
- REST APIs REST API Authentication: API Keys, JWT, and OAuth 2.0
Learn the three most common REST API authentication methods. Compare API keys, JWT tokens, and OAuth 2.0 with working code examples and security best practices.
- REST APIs REST API Caching: ETags, Cache-Control, and CDN Patterns
Master HTTP caching for REST APIs. Learn ETags, Cache-Control headers, conditional requests, and CDN integration patterns with practical examples.
- REST APIs REST vs GraphQL vs gRPC: API Styles Compared
Compare REST, GraphQL, and gRPC for API design. Understand tradeoffs in performance, flexibility, and developer experience to pick the right API style.
- REST APIs REST API Error Handling Conventions
Design clear, consistent error responses for REST APIs using HTTP status codes, problem details, and error envelopes that clients can actually handle.