Request Helpers
Reducing boilerplate
Integration tests make many HTTP requests. Building a new Request(...) with headers, body, and method every time is verbose. Request helpers encapsulate common patterns.
The core helper
// tests/helpers/request.ts
export function jsonRequest(
method: string,
path: string,
body?: unknown,
headers?: Record<string, string>,
): Request {
return new Request(`http://localhost${path}`, {
method,
headers: {
"Content-Type": "application/json",
...headers,
},
body: body ? JSON.stringify(body) : undefined,
});
} This was introduced in the Route Handlers lesson. Now we build on it.
Shorthand methods
export function get(path: string, headers?: Record<string, string>): Request {
return new Request(`http://localhost${path}`, { headers });
}
export function post(path: string, body: unknown, headers?: Record<string, string>): Request {
return jsonRequest("POST", path, body, headers);
}
export function put(path: string, body: unknown, headers?: Record<string, string>): Request {
return jsonRequest("PUT", path, body, headers);
}
export function patch(path: string, body: unknown, headers?: Record<string, string>): Request {
return jsonRequest("PATCH", path, body, headers);
}
export function del(path: string, headers?: Record<string, string>): Request {
return new Request(`http://localhost${path}`, { method: "DELETE", headers });
} Tests become concise:
// Before
const response = await app.fetch(
new Request("http://localhost/v2/books", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer tok" },
body: JSON.stringify({ title: "Kindred", authorId: "a1", genre: "fiction" }),
}),
);
// After
const response = await app.fetch(
post("/v2/books", { title: "Kindred", authorId: "a1", genre: "fiction" }, authHeader("u1")),
); Assertion helpers
export async function expectJson(response: Response, status: number = 200) {
expect(response.status).toBe(status);
expect(response.headers.get("content-type")).toContain("application/json");
return response.json();
}
export async function expectError(response: Response, status: number) {
expect(response.status).toBe(status);
const body = await response.json();
expect(body.error).toBeDefined();
return body;
} // Before
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toContain("application/json");
const data = await response.json();
// After
const data = await expectJson(response); Putting it all together
import { app } from "../../src/app.js";
import { get, post, del } from "../helpers/request.js";
import { expectJson, expectError } from "../helpers/request.js";
import { authHeader } from "../helpers/auth.js";
import { createBook, createAuthor } from "../helpers/factories.js";
describe("book CRUD", () => {
const author = () => createAuthor({ name: "Butler" });
test("list → create → get → delete", async () => {
const a = author();
// List (empty)
const list1 = await expectJson(await app.fetch(get("/v2/books")));
expect(list1).toHaveLength(0);
// Create
const created = await expectJson(
await app.fetch(
post(
"/v2/books",
{ title: "Kindred", authorId: a.id, genre: "science-fiction" },
authHeader("u1"),
),
),
201,
);
expect(created.title).toBe("Kindred");
// Get
const fetched = await expectJson(await app.fetch(get(`/v2/books/${created.id}`)));
expect(fetched.title).toBe("Kindred");
// Delete
const deleteRes = await app.fetch(del(`/v2/books/${created.id}`, authHeader("u1", "admin")));
expect(deleteRes.status).toBe(204);
// Verify deleted
const notFound = await app.fetch(get(`/v2/books/${created.id}`));
expect(notFound.status).toBe(404);
});
}); Clean, readable, focused on behavior. The helpers handle the HTTP mechanics.
Exercises
Exercise 1: Build get, post, put, patch, del helpers. Use them to rewrite an existing test.
Exercise 2: Build expectJson and expectError. Use them to simplify response assertions.
Exercise 3: Write a full CRUD test (list → create → get → update → delete → verify deleted) using all helpers.
Why build request and assertion helpers?