Q9 of 37 · API testing

How do you test that an API endpoint returns the correct response?

API testingJuniorapiassertionstestingfundamentals

Short answer

Short answer: Send the request with a known input, assert four things: status code, response shape (schema), key field values, and timing if relevant. Use tools like Postman, REST Assured, or Playwright APIRequestContext. Cover happy path + 1-2 error cases.

Detail

A complete API test asserts on more than just the body — it covers the contract holistically.

The four assertions every API test should make (where applicable):

  1. Status code — the documented one. expect(res.status).toBe(200). If 201 is documented, 200 is a failure even with the right body.

  2. Response shape — does it have the expected fields and types? Either field-by-field (expect(body.user.id).toBeDefined()) or holistically with a JSON Schema.

  3. Key field values — for the inputs you sent, the response is correct. expect(body.email).toBe('alice@example.com').

  4. Headers — when relevant. Content-Type, Cache-Control, X-Request-ID for debugging traceability.

A complete example:

test('GET /users/:id returns the user', async ({ request }) => {
  const res = await request.get('/users/42');

  expect(res.status()).toBe(200);
  expect(res.headers()['content-type']).toContain('application/json');

  const body = await res.json();
  expect(body).toMatchObject({
    id: '42',
    email: expect.stringMatching(/^.+@.+$/),
    role: expect.any(String),
  });
});

What to cover beyond the happy path (for any meaningful endpoint):

  • 404 — request a non-existent ID.
  • 401 — request without an auth token.
  • 403 — request as a user without permission.
  • 400 — request with invalid input (missing required field, bad type).
  • Edge values — boundary cases (empty list, max-length strings, very long IDs).

What junior tests often miss:

  • Schema instead of fields — write the schema once and validate every response against it. Misses fewer regressions than ad-hoc field checks.
  • Test isolation — each test should set up its own data, not rely on previous tests' results.
  • Negative cases — happy path passes 100% of the time, but real bugs are usually in error paths. Cover at least 401 and 400 alongside 200.

The interview signal: writing tests that assert the contract, not just "the call worked."

// EXAMPLE

users.api.test.js

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

test('GET /users/:id', async ({ request }) => {
  const res = await request.get('https://api.example.com/users/42', {
    headers: { Authorization: `Bearer ${process.env.API_TOKEN}` },
  });

  // 1. Status
  expect(res.status()).toBe(200);

  // 2. Headers
  expect(res.headers()['content-type']).toContain('application/json');

  // 3. Shape and 4. Values
  const body = await res.json();
  expect(body).toMatchObject({
    id: '42',
    email: expect.stringMatching(/@/),
    active: true,
  });
});

// WHAT INTERVIEWERS LOOK FOR

All four assertions (status, headers, shape, values), covering at least one negative case, and bonus awareness of schema-based validation over field-by-field.

// COMMON PITFALL

Asserting only on status code 200 and assuming the body is correct. The body could be empty, malformed, or contain wrong values — the test would still pass.