Skip to main content
← All Articles · · 12 min read

API Testing in 2026: From cURL to Automated Test Suites

Complete API testing guide 2026: cURL basics to Postman, Insomnia, and automated suites with JavaScript and Python. Real examples included.

apitestingcurlhttppostmanautomation

API Testing in 2026: From cURL to Automated Test Suites

Every developer who builds backends or integrates third-party services needs to test APIs. But “testing an API” means different things at different stages: exploratory testing while building, regression testing before releases, and contract testing between teams. This guide covers all three levels.

We’ll go from the lowest level (cURL in your terminal) up to automated test suites that run in CI. Each level adds confidence that your API actually does what you think it does.

Why Start with cURL

Before GUI tools like Postman or automated test frameworks, start with cURL. Not because it’s the best tool long-term, but because it forces you to understand what’s actually happening at the HTTP layer. No abstractions. No auto-generated headers. Just raw requests.

When you understand cURL, Postman is just a prettier wrapper. When something breaks in a test framework, you can always drop down to cURL to verify the actual HTTP behavior.

# GET request
curl https://api.example.com/users

# GET with headers
curl -H "Authorization: Bearer $TOKEN" \
     -H "Accept: application/json" \
     https://api.example.com/users/123

# POST with JSON body
curl -X POST https://api.example.com/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice", "email": "[email protected]"}'

# PUT
curl -X PUT https://api.example.com/users/123 \
  -H "Content-Type: application/json" \
  -d '{"name": "Alice Updated"}'

# DELETE
curl -X DELETE https://api.example.com/users/123

# Show response headers
curl -i https://api.example.com/users/123

# Save response to file
curl -s https://api.example.com/users > users.json

# Pipe to jq for pretty printing
curl -s https://api.example.com/users | jq .

The -s flag silences the progress meter. The -v flag shows the full request and response headers — use this when debugging unexpected behavior.

Real API Testing Workflow with cURL

Here’s a realistic auth-then-resource flow:

# 1. Login and extract token using jq
TOKEN=$(curl -s -X POST https://api.example.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"secret"}' \
  | jq -r '.token')

# 2. Use token to get protected resource
curl -s -H "Authorization: Bearer $TOKEN" \
     https://api.example.com/profile | jq .

# 3. Create a resource
NEW_USER_ID=$(curl -s -X POST https://api.example.com/users \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Test User"}' \
  | jq -r '.id')

# 4. Verify it was created
curl -s -H "Authorization: Bearer $TOKEN" \
     https://api.example.com/users/$NEW_USER_ID | jq .

# 5. Clean up
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
     https://api.example.com/users/$NEW_USER_ID

HTTP Status Codes You Must Know

Understanding status codes is fundamental. Don’t memorize all 70+ codes — focus on these:

CodeMeaningWhen it happens
200OKSuccessful GET, PATCH, PUT
201CreatedSuccessful POST that creates a resource
204No ContentSuccessful DELETE or PUT with no body response
400Bad RequestInvalid JSON, missing required fields, failed validation
401UnauthorizedMissing token, expired token, invalid token
403ForbiddenValid token, but insufficient permissions
404Not FoundResource doesn’t exist
409ConflictDuplicate resource (email already registered)
422Unprocessable EntityRequest understood, but validation failed
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side bug

A 401 means “you didn’t identify yourself.” A 403 means “you identified yourself, but you’re not allowed.” This distinction matters for security and debugging.

GUI Tools: Postman vs Bruno vs Insomnia

Once you’ve verified basics with cURL, GUI tools speed up exploratory testing.

Postman is the industry standard. It has collections, environments, pre-request scripts, test assertions, and team collaboration.

Bruno is the new alternative (open-source, local-first). It stores collections as plain text files you can commit to your repository — much better for version control than Postman’s proprietary format.

Insomnia is a simpler alternative with a clean UI, good for HTTP and GraphQL.

For teams, consider Bruno or Postman collections committed to your repo alongside your API code.

Automated Tests with JavaScript (Playwright)

For APIs that need regression testing in CI, Playwright’s request API is excellent:

import { test, expect } from '@playwright/test';

const BASE_URL = process.env.API_URL || 'https://api.example.com';
let authToken;

test.beforeAll(async ({ request }) => {
  const response = await request.post(`${BASE_URL}/auth/login`, {
    data: { email: '[email protected]', password: 'secret' }
  });
  expect(response.status()).toBe(200);
  const body = await response.json();
  authToken = body.token;
});

test('GET /users returns 200 and array', async ({ request }) => {
  const response = await request.get(`${BASE_URL}/users`, {
    headers: { Authorization: `Bearer ${authToken}` }
  });
  expect(response.status()).toBe(200);
  const users = await response.json();
  expect(Array.isArray(users)).toBe(true);
  expect(users.length).toBeGreaterThan(0);
  expect(users[0]).toMatchObject({
    id: expect.any(Number),
    name: expect.any(String),
    email: expect.any(String)
  });
});

test('POST /users with missing email returns 400', async ({ request }) => {
  const response = await request.post(`${BASE_URL}/users`, {
    headers: { Authorization: `Bearer ${authToken}` },
    data: { name: 'No Email User' }
  });
  expect(response.status()).toBe(400);
  const error = await response.json();
  expect(error.message).toMatch(/email/i);
});

test('GET /users without auth returns 401', async ({ request }) => {
  const response = await request.get(`${BASE_URL}/users`);
  expect(response.status()).toBe(401);
});

Python Tests with pytest + httpx

import pytest
import httpx

BASE = "https://api.example.com"

@pytest.fixture(scope="session")
def auth_headers():
    response = httpx.post(f"{BASE}/auth/login", json={
        "email": "[email protected]",
        "password": "secret"
    })
    assert response.status_code == 200
    token = response.json()["token"]
    return {"Authorization": f"Bearer {token}"}


def test_get_users_returns_200(auth_headers):
    r = httpx.get(f"{BASE}/users", headers=auth_headers)
    assert r.status_code == 200
    assert isinstance(r.json(), list)


def test_missing_email_returns_400(auth_headers):
    r = httpx.post(f"{BASE}/users", json={"name": "No Email"}, headers=auth_headers)
    assert r.status_code == 400
    assert "email" in r.json().get("message", "").lower()


def test_unauthenticated_returns_401():
    r = httpx.get(f"{BASE}/users")
    assert r.status_code == 401

Edge Cases Every API Test Suite Must Cover

Beginners test the happy path and stop. Senior developers know the bugs live in edge cases:

1. Empty body on POST → expect 400, not 500
2. Invalid JSON (malformed) → expect 400
3. Missing required fields → expect 400 with field names in error
4. Wrong content-type header → expect 400 or 415
5. Expired/invalid token → expect 401
6. Valid token with insufficient permissions → expect 403
7. Resource ID that doesn't exist → expect 404
8. Duplicate resource (create same email twice) → expect 409
9. Very long strings in fields → expect 400 or graceful truncation
10. Concurrent requests to the same resource → check for race conditions
11. Rate limit exceeded (429) — does the API return a Retry-After header?

Testing for these cases protects you from bugs that only appear in production under real user behavior.

Contract Testing with Zod

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string().min(1),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

test('GET /users response matches contract', async ({ request }) => {
  const response = await request.get('/users');
  expect(response.status()).toBe(200);
  const body = await response.json();
  // Throws ZodError if shape doesn't match
  const validated = z.array(UserSchema).parse(body);
  expect(validated.length).toBeGreaterThan(0);
});

Contract tests catch breaking changes early — when a backend developer renames userId to user_id, your contract test fails before it breaks the frontend.

Setting Up API Tests in CI

- name: Run API tests
  env:
    API_URL: ${{ secrets.STAGING_API_URL }}
    TEST_EMAIL: ${{ secrets.TEST_USER_EMAIL }}
    TEST_PASSWORD: ${{ secrets.TEST_USER_PASSWORD }}
  run: npm run test:api:ci

Keep test credentials in secrets, not in the codebase.

Key Takeaways

  • Start with cURL for exploration — it shows you raw HTTP with no abstractions.
  • Know the difference between 401 (unauthenticated) and 403 (unauthorized).
  • Use GUI tools (Postman/Bruno) for team collaboration and manual testing.
  • Write automated tests for regression coverage — test the happy path AND edge cases.
  • Test schema/contract to catch breaking changes before they hit production.
  • Run API tests in CI against a staging environment.

FAQ

Should I use Postman or code-based tests? Both. Postman for interactive exploration. Code-based tests for regression coverage that runs automatically in CI.

Is mocking the API enough? Mocks are fine for unit testing logic, but they can give false confidence. Integration tests against a real server catch bugs that mocks miss.

How do I test file uploads?

curl -X POST https://api.example.com/upload \
  -H "Authorization: Bearer $TOKEN" \
  -F "file=@/path/to/file.pdf"

What’s the best way to handle test data cleanup? Either use a dedicated test database that gets reset between runs, or have your tests clean up after themselves (create a resource in setup, delete it in teardown).

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.