Test Automation 8 min read

API Testing: Strategy, Tools & Examples

Everything you need to know about API testing — why it matters, REST vs GraphQL testing strategies, tools comparison (Postman, REST Assured, Playwright).

I
InnovateBits
InnovateBits

API testing is the highest-leverage investment in your QE strategy. While UI tests are slow, brittle, and expensive to maintain, API tests are fast, stable, and directly validate the business logic your product depends on. If your team is spending most of its automation budget on UI tests, this guide will help you rebalance toward the API layer — where the return on investment is significantly higher.


Why API Testing Matters

The traditional test automation pyramid puts unit tests at the base, integration/API tests in the middle, and UI tests at the top — with a warning to keep the top layer thin.

The reasoning is practical:

LayerSpeedStabilityCoverageCost
Unit tests~msHighNarrowLow
API tests~secondsHighBroadMedium
UI tests~minutesLowEnd-to-endHigh

API tests give you broad coverage at high speed and stability. A well-designed API test suite can validate the behaviour of an entire backend in under two minutes — something a UI suite for the same functionality might take 30 minutes to do, with far more intermittent failures.

Beyond speed, API testing catches a specific category of defect that UI testing misses: contract violations. When a backend team changes a response schema without telling the frontend team, UI tests might not catch it until QA manually explores the feature. API tests catch it in seconds.


Understanding What to Test

Functional testing

Verify that each endpoint does what it's supposed to do:

  • Returns the correct status code (200, 201, 400, 401, 404, 500)
  • Returns the correct response body structure and data types
  • Handles valid inputs correctly
  • Returns meaningful errors for invalid inputs
  • Enforces authentication and authorisation correctly

Contract testing

Ensure the API schema doesn't break unexpectedly. Tools like Pact (consumer-driven contract testing) and Dredd (OpenAPI/Swagger spec validation) automate this.

Performance testing

Validate that endpoints respond within acceptable time limits under expected load. A GET endpoint that takes 3 seconds is functionally correct but operationally broken. See our guide on performance testing for a deeper look at this area.

Security testing

Check that authentication is enforced, tokens expire correctly, rate limiting works, and sensitive data isn't leaked in responses.


Tools Comparison

Postman / Newman

Best for: Exploratory testing, team collaboration, contract documentation

Postman is the most widely used API testing tool. Its GUI makes it approachable for testers without deep coding experience. Collections can be exported and run in CI via Newman:

BASH
1# Install Newman 2npm install -g newman 3 4# Run a collection 5newman run MyAPI.postman_collection.json \ 6 --environment staging.postman_environment.json \ 7 --reporters cli,junit \ 8 --reporter-junit-export results.xml

Postman's biggest limitation is that collection scripts (written in a restricted JavaScript sandbox) are harder to maintain than a proper codebase as tests grow.

REST Assured (Java)

Best for: Java shops, complex test logic, integration with JUnit/TestNG

REST Assured has a fluent DSL that makes API tests readable and expressive:

JAVA
1@Test 2public void getUser_returnsCorrectName() { 3 given() 4 .header("Authorization", "Bearer " + authToken) 5 .pathParam("id", 123) 6 .when() 7 .get("/api/users/{id}") 8 .then() 9 .statusCode(200) 10 .body("name", equalTo("Alice")) 11 .body("email", containsString("@")) 12 .time(lessThan(1000L)); // Response under 1 second 13}

Playwright (TypeScript/JavaScript)

Best for: Teams already using Playwright for UI tests, full-stack test suites

Playwright has a built-in APIRequestContext that's ideal if you want UI and API tests in the same framework:

TYPESCRIPT
1import { test, expect } from '@playwright/test'; 2 3test('GET /api/users returns user list', async ({ request }) => { 4 const response = await request.get('/api/users', { 5 headers: { Authorization: `Bearer ${process.env.API_TOKEN}` } 6 }); 7 8 expect(response.status()).toBe(200); 9 10 const body = await response.json(); 11 expect(body.users).toBeInstanceOf(Array); 12 expect(body.users.length).toBeGreaterThan(0); 13 expect(body.users[0]).toMatchObject({ 14 id: expect.any(Number), 15 name: expect.any(String), 16 email: expect.stringContaining('@'), 17 }); 18});

Supertest (Node.js)

Best for: Node.js backends, testing the app layer directly without HTTP overhead

TYPESCRIPT
1import request from 'supertest'; 2import app from '../app'; // Your Express/Fastify app 3 4describe('POST /api/users', () => { 5 it('creates a user and returns 201', async () => { 6 const res = await request(app) 7 .post('/api/users') 8 .set('Authorization', `Bearer ${token}`) 9 .send({ name: 'Alice', email: 'alice@example.com' }); 10 11 expect(res.status).toBe(201); 12 expect(res.body.id).toBeDefined(); 13 expect(res.body.name).toBe('Alice'); 14 }); 15 16 it('returns 400 for missing email', async () => { 17 const res = await request(app) 18 .post('/api/users') 19 .set('Authorization', `Bearer ${token}`) 20 .send({ name: 'Alice' }); 21 22 expect(res.status).toBe(400); 23 expect(res.body.error).toMatch(/email/i); 24 }); 25});

Building a Practical API Test Suite

Structure your tests by resource

Organise test files to mirror your API structure. This makes it easy to find tests when an endpoint changes:

tests/
  api/
    auth/
      login.spec.ts
      token-refresh.spec.ts
    users/
      get-user.spec.ts
      create-user.spec.ts
      update-user.spec.ts
      delete-user.spec.ts
    orders/
      get-orders.spec.ts
      create-order.spec.ts

Authentication patterns

Don't duplicate auth setup in every test. Use a shared fixture:

TYPESCRIPT
1// fixtures/auth.ts 2export async function getAuthToken(request: APIRequestContext): Promise<string> { 3 const response = await request.post('/api/auth/login', { 4 data: { 5 email: process.env.TEST_EMAIL, 6 password: process.env.TEST_PASSWORD, 7 } 8 }); 9 const { token } = await response.json(); 10 return token; 11} 12 13// playwright.config.ts — set token as a fixture available to all tests 14// or pass it via environment variables in CI

Test data management

The hardest problem in API testing is test data. Tests that share a database state interfere with each other and produce inconsistent results.

The cleanest solution is to create and clean up test data within each test:

TYPESCRIPT
1test('delete user removes them from the system', async ({ request }) => { 2 // Create test data as part of this test 3 const createResponse = await request.post('/api/users', { 4 headers: { Authorization: `Bearer ${token}` }, 5 data: { name: 'Test User', email: `test-${Date.now()}@example.com` } 6 }); 7 const { id } = await createResponse.json(); 8 9 // Test the delete 10 const deleteResponse = await request.delete(`/api/users/${id}`, { 11 headers: { Authorization: `Bearer ${token}` } 12 }); 13 expect(deleteResponse.status()).toBe(204); 14 15 // Verify deletion 16 const getResponse = await request.get(`/api/users/${id}`, { 17 headers: { Authorization: `Bearer ${token}` } 18 }); 19 expect(getResponse.status()).toBe(404); 20});

Using Date.now() or a UUID in test data ensures each test run uses fresh, non-conflicting data.


GraphQL API Testing

GraphQL requires a slightly different approach — all requests go to a single endpoint, and the query structure determines what you get back.

TYPESCRIPT
1test('fetch user with posts', async ({ request }) => { 2 const query = ` 3 query GetUser($id: ID!) { 4 user(id: $id) { 5 id 6 name 7 email 8 posts { 9 title 10 publishedAt 11 } 12 } 13 } 14 `; 15 16 const response = await request.post('/graphql', { 17 headers: { 18 Authorization: `Bearer ${token}`, 19 'Content-Type': 'application/json', 20 }, 21 data: { 22 query, 23 variables: { id: '123' } 24 } 25 }); 26 27 expect(response.status()).toBe(200); 28 const { data, errors } = await response.json(); 29 expect(errors).toBeUndefined(); 30 expect(data.user.name).toBe('Alice'); 31 expect(data.user.posts).toBeInstanceOf(Array); 32});

For GraphQL specifically, always check the errors field even when the HTTP status is 200 — GraphQL returns errors in the response body, not as HTTP error codes.


Integrating API Tests into CI/CD

API tests should run on every pull request and be the first gate in your pipeline — they're fast enough to not block developers.

A minimal GitHub Actions setup:

YAML
1name: API Tests 2on: [push, pull_request] 3 4jobs: 5 api-tests: 6 runs-on: ubuntu-latest 7 services: 8 postgres: 9 image: postgres:16 10 env: 11 POSTGRES_DB: testdb 12 POSTGRES_USER: test 13 POSTGRES_PASSWORD: test 14 ports: ['5432:5432'] 15 steps: 16 - uses: actions/checkout@v4 17 - uses: actions/setup-node@v4 18 with: { node-version: 20 } 19 - run: npm ci 20 - run: npm run db:migrate:test 21 env: { DATABASE_URL: postgres://test:test@localhost:5432/testdb } 22 - run: npx playwright test tests/api/ 23 env: 24 BASE_URL: http://localhost:3000 25 TEST_EMAIL: ${{ secrets.TEST_EMAIL }} 26 TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}

Common Mistakes to Avoid

Testing implementation details. Assert on what the API returns, not on how the database stores it internally. Tests that reach into the DB to verify state are tightly coupled to implementation.

Ignoring error responses. Teams often test the happy path thoroughly and skip error cases. But error handling logic has bugs too — and those bugs surface in production.

Using shared test accounts. When multiple CI runs execute in parallel using the same test user, they interfere with each other. Use dynamic test data or isolated test users per run.

Not validating response schemas. Checking response.status === 200 is necessary but not sufficient. Validate the shape of the response body — a field changing from a number to a string is a breaking change that a status code check won't catch.


Next Steps

A solid API test suite is the backbone of a high-confidence CI/CD pipeline. With fast, stable API tests as your foundation, you can run UI tests selectively — on the flows where end-to-end validation genuinely adds value — rather than as the primary regression gate.

For the test sites to practice API testing on, see our API Testing Demo Sites guide. And once your API suite is running, explore how to combine it with Playwright's UI testing to build a complete test pyramid.

Tags
#api-testing#rest-api#postman#playwright#test-automation

Share this article

Follow for more

Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!