# Functional API Tests: What to Assert — Routebase

> How to choose assertions that catch real breakage without turning every unrelated change into a red run, plus negative cases, extraction and chaining.

Canonical page: https://routebase.dev/guides/api-testing/functional-api-tests/
Chapter 3 of 12 · API Testing · Last reviewed 2026-09-12 · The Routebase Team

Functional testing is where most REST API testing starts, and it is the method most likely to be done in a way that quietly stops working. The mechanics are trivial, because you send a request and check the answer. The judgement is not, because what you check decides whether the suite is a signal or a chore.

## The assertion budget

Think of each test as having a budget. Every assertion you spend buys coverage of one failure mode and costs maintenance on every unrelated change that happens to touch it.

Spend too little and the test passes while the API degrades. A case asserting only that the status was 200 stays green while the body loses half its fields. It stays green when the total is wrong by a factor of a hundred, and when the endpoint starts returning an empty array for every query.

Spend too much and the test fails constantly for reasons nobody cares about. Whole-body comparisons are the classic overspend, because they assert on generated identifiers, timestamps, field ordering and every property somebody adds later. Three false alarms is roughly how long it takes a team to start pressing rerun and stop investigating.

_Figure: Four of these five readings of one response are worth asserting, and the fifth is the full-body comparison that turns every unrelated change into a red run._

## What a well-shaped case asserts

| Assertion | Why it earns its place |
|---|---|
| Status code | The cheapest signal there is, and the first thing that changes when something breaks |
| Schema against the contract | Catches every shape change at once, without naming a single field by hand |
| Two or three field values | Proves the behaviour this test is actually about |
| Content type | Matters when clients dispatch on it, and it is a one-line check |
| A latency ceiling | A rough guard that turns a pathological regression into a failure instead of a slow green |

The schema line is what lets the field assertions stay few. Without it you end up naming fields defensively, because that is the only way to notice a rename. With it the shape is covered structurally, and the remaining assertions can concentrate on meaning.

## Assert on values, not on documents

Two ways to check that a response contains the right customer name.

```txt
# Fragile
Body equals {"id":"cus_31","name":"Ada Lovelace","createdAt":"2026-09-12T09:14:22Z", ...}

# Durable
JSON Path  $.name  equals  Ada Lovelace
```

The second one fails for exactly one reason, and the failure message names the field, the expected value and what actually arrived. The first one fails whenever anything in the document changes, and the failure message is a diff you have to read.

There is one case where the whole-document check is right, and it is narrower than people think. The response is a small, fully specified, stable object, and you genuinely mean that nothing else may appear. Such responses exist, and they are far rarer than the number of tests written that way suggests.

## Negative cases are half the suite

An API that handles valid input correctly and undefined input unpredictably is half tested. The unpredictable half is where customers meet the bugs, because their client sends the malformed request, not yours.

Four paths deserve a case on nearly every endpoint that has them.

| Path | Expected | What it proves |
|---|---|---|
| Missing or malformed input | 400 with a described error body | Validation runs before the handler, and the error is machine-readable |
| No credential | 401 | The endpoint is actually protected, and not only in the router config you last read |
| Valid credential, wrong permission | 403 | Authorization is enforced per caller and not only per route |
| Unknown identifier | 404 | The service does not leak a 500, and it does not silently return an empty success |

The 401 and 403 cases deserve special attention, because they are the two assertions most often assumed and never tested. An endpoint that returns 200 to an unauthenticated caller is a production incident, and the test that would have caught it is one line long.

Error bodies deserve a shape check of their own, for the same reason successful ones do. If your errors follow a documented format, validate them against it. An error path that returns a bare string while the contract promises a structured body breaks clients exactly when they are already having a bad day.

## Extraction and chaining

Many tests need something a previous request produced, such as the identifier of a resource you just created. Hard-coding that identifier makes the test depend on data somebody else maintains. That is the most common source of a suite which works on one environment and not another.

Extraction solves the problem without any shared setup. The create case captures a value from its response, and every later case references it by name. The chain then owns its data, so it runs anywhere without a prepared database.

```txt
Case 1  POST /orders          extract  $.id          →  orderId
Case 2  GET  /orders/{{orderId}}   assert  $.status  equals  pending
```

Three sources cover nearly everything. Those are a value from the JSON body selected by JSONPath, a response header, and the status code itself. The status code is useful more often than people expect, because it lets a later step branch on what happened and not on what was supposed to happen.

## Keeping runs deterministic

Three habits account for most of the difference between a suite people trust and one they rerun.

**Own your data.** A test that reads a record somebody created by hand in staging will fail the week that record is cleaned up. Create what you need, reference it by the identifier you extracted, and clean it up afterwards.

**Do not assert on things the API never promised.** Array ordering without an explicit sort is not contractual, and neither is the exact value of a generated identifier or the precise timestamp of an action. Asserting on any of them is asserting on an accident.

**Use dynamic values for anything that must be unique.** A test that registers `test@example.com` works once. A test that registers a generated address works every time, which matters more than it sounds when the same suite runs on a schedule.

## In Routebase

A test case in Routebase is one HTTP request with a set of declarative assertions, so there are no test scripts to maintain for ordinary work. Six assertion types cover the whole ground between a status code and a schema. Those are Status Code, Body, Header, Latency in milliseconds, JSON Path and Schema Validation. Each one combines with ten operators, from `equals` and `contains` through `matches regex` and `exists` to the four numeric comparisons.

_Screenshot: Five assertions cover the status code, one header, one field, the latency budget and the response schema._

Schema Validation is the one that needs no target and no expected value, because it validates the response against the linked endpoint's documented schema. That is the assertion that keeps the others few. Every assertion carries an on and off checkbox, so you can mute one without deleting it.

When a case is linked to a designed endpoint, the JSON Path field suggests paths from that endpoint's response schema, each with its type, and free text stays available for expressions the schema cannot enumerate. The Extract tab captures values into variables from the JSON body, a response header or the status code, and later cases reference them as `{{variableName}}`. Dynamic tokens such as `{{$uuid}}`, `{{$timestamp}}` and `{{$randomEmail}}` handle the values that must differ every run.

Results show the assertions that passed and failed side by side with expected against actual, the exact request that went out after substitution, and a timing breakdown across DNS, connect, TLS, first byte and download. The full reference is in the [Test Suites guide](https://docs.routebase.dev/test-suites/).

## Frequently asked questions

### What should an API test assert?

Assert the status code, the shape of the response against the contract, and the two or three field values that carry the behaviour this test exists to prove. Add a latency ceiling as a rough guard and a content-type check where the media type matters. Everything beyond that tends to cost more in maintenance than it returns in defects caught, because it fails on changes that were never the point of the test.

### Why do full response body comparisons make tests flaky?

A whole-body comparison asserts on every field, including generated identifiers, timestamps, ordering the API never promised, and any field somebody adds next quarter. Each of those produces a red run that has nothing to do with a defect. After the third false alarm people start rerunning instead of investigating, and the suite stops being a signal.

### How do you test error responses in an API?

Write a case per error path you care about, with the status code as the assertion and the error body shape validated like any other response. Four cases belong on nearly every endpoint. Malformed input should return 400, an absent credential should return 401, a valid credential without permission should return 403, and an unknown identifier should return 404. Those four paths are where undefined behaviour reaches customers most often.

### Should API tests use JSONPath assertions?

Yes, for the specific values a test is about. A JSONPath assertion names one field, so it fails for one reason and the failure message says which field and what value arrived. That is far easier to act on than a diff of two large JSON documents, and it survives unrelated additions to the response.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
