Asserting status, body and schema
An automated API test is only as good as what it checks. A test that sends a request and asserts nothing useful — or only the status code — passes while real bugs slip through. This lesson is asserting well on an API response: the status, the body's values, and its schema (the contract from the api-testing module), now automated so the contract is verified on every run.
Assert the status — the right one
The first check is always the status code, and the discipline from the api-testing module carries straight over: assert the right status for the case, not just "not an error".
expect(res.statusCode).toBe(201); // created — not 200, not 500
expect(res.statusCode).toBe(422); // invalid input rejected on purpose
expect(res.statusCode).toBe(403); // authenticated but not allowed
AgentPay's tests are precise about this: create → 201, bad amount → 422, wrong-agent access → 403, suspended agent → 409, missing token → 401. Asserting the exact status is what catches "returns 200 for input it should reject" and "returns 500 where a 400 was due" — the classic API bugs.
Assert the body — the values, not just the shape
A green status with wrong data is still a bug (the contracts lesson). So check the body's actual values:
const body = res.json();
expect(body).toMatchObject({ agentId: 1, amount: 500, status: 'success' });
toMatchObject asserts the fields you care about are present and correct, ignoring the rest (like the
generated id and createdAt) — useful when you do not want to pin every field. For exact checks use
toEqual; for one field, expect(body.status).toBe('success'). AgentPay's dashboard test goes further and
asserts a computed property — that the status counts add up to the total:
const { success, pending, failed } = body.byStatus;
expect(success + pending + failed).toBe(body.count); // an aggregate that must be internally consistent
That is a strong kind of assertion: not just "a number came back" but "the numbers are correct relative to each other" — the sort of check that catches a wrong aggregate a shape check would miss.
Assert relationships and collections
API responses are often lists, and you assert properties across them — the same "only-and-all" thinking as the manual filter tests, in code:
// filtering: every returned row matches the filter
const data = res.json().data;
expect(data.every((t) => t.status === 'failed')).toBe(true);
// pagination: no row dropped or duplicated across pages
expect(seen.size).toBe(60);
// sorting: numerically ordered, not alphabetically
const amounts = data.map((t) => t.amount);
expect(amounts).toEqual([...amounts].sort((a, b) => a - b));
These are AgentPay's real list assertions. Notice they check the property that must hold (all rows match, none repeat, order is correct) rather than hard-coding expected rows — robust to the exact seed, and they express what "correct" actually means.
Assert the schema — verify the contract
The most valuable automated API check is schema validation: does the response match its agreed contract (fields, types, allowed values)? Automated, it runs on every request and catches the quiet contract breaks — a renamed field, a type change, a stray null — that return 200 but break the app (the contracts lesson).
You can assert types field by field:
expect(typeof body.id).toBe('number');
expect(typeof body.amount).toBe('number'); // not the string "500"
expect(['success', 'pending', 'failed']).toContain(body.status);
or, better for a whole response, validate against a JSON Schema with a library (Ajv, Zod, or your framework's validator). AgentPay's API is built with Zod schemas for its inputs; you can reuse the same idea to validate responses in tests — define the expected shape once and assert every response against it:
const TxnSchema = z.object({
id: z.number(),
agentId: z.number(),
amount: z.number(),
status: z.enum(['success', 'pending', 'failed']),
createdAt: z.string(),
});
expect(() => TxnSchema.parse(res.json())).not.toThrow(); // the whole contract, in one assertion
One assertion, and any drift in the response shape fails the test loudly. This is the automated version of the contract testing you met by hand — and it is the single highest-leverage API assertion you can add.
Don't over-assert, don't under-assert
A balance worth naming:
- Under-asserting — checking only the status — lets wrong data through. Always check the body too.
- Over-asserting — pinning every field including generated ids and timestamps — makes tests brittle:
they fail on harmless changes and become noise. Assert what matters for the case (
toMatchObject, a schema, the specific values), and let incidental fields vary.
The goal is assertions that fail when something is actually wrong and stay green otherwise — the same reliability principle as the whole automation part. A test that fails on a new timestamp format teaches people to ignore failures, exactly like a flaky one.
Check your work
Status: assert the right one per case (201/422/403/409/401), not just "not an error" — catches the 200-for-bad-input and 500-for-bad-input bugs.
Body values: a green status with wrong data is a bug; use toMatchObject (fields you care about),
toEqual (exact), or specific-field checks. Assert computed correctness where you can — AgentPay checks the
status counts sum to the total.
Collections/relationships: assert the property that must hold across rows — all match a filter, none dropped/duplicated across pages, numeric/chronological order — not hard-coded rows.
Schema (the contract): the highest-value check — validate the response's shape/types/enums (field-by- field, or a JSON Schema via Ajv/Zod in one assertion) so quiet contract breaks that still return 200 fail loudly. The automated form of contract testing.
Balance: don't under-assert (status only — misses wrong data) or over-assert (every generated field — brittle). Assert what matters; fail only when something is actually wrong.
Practice
- For AgentPay's create endpoint, assert the exact status and the body with
toMatchObject; then break the expected amount and watch it fail. - Automate the dashboard aggregate check: assert
byStatuscounts sum tocount. - Write a filter test that asserts every returned row matches the filter (not specific rows).
- Write a pagination test that asserts no id is dropped or duplicated across all pages.
- Define a JSON Schema (or Zod schema) for the transaction response and assert a response against it; then change a field's type in the app and confirm the schema assertion fails.
- Take an over-asserting test (pins
createdAt) and relax it to assert only what matters; explain why.
Official documentation
- Vitest — Expect / assertions —
toBe,toEqual,toMatchObject,toContain. - Ajv — JSON Schema validation — Validating a response against a JSON Schema.
- Zod — Basic usage — Defining a schema and parsing/validating a response in tests.
Next: auth, and arranging state through the API.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship