hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Testing APIs with @hectoday/http

Why Test

  • What Testing Gives You
  • Types of Tests
  • Project Setup

Unit Testing

  • Testing Pure Functions
  • Testing Zod Schemas
  • Testing Business Logic

Integration Testing

  • Testing Route Handlers
  • Testing GET Endpoints
  • Testing POST Endpoints
  • Testing Error Responses
  • Testing Authentication

Test Helpers

  • Factories and Fixtures
  • Test Database Isolation
  • Request Helpers

Advanced Testing

  • Mocking External Services
  • Testing Background Jobs
  • Testing Edge Cases

Putting It All Together

  • Test Organization
  • Checklist and Capstone

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?

← Test Database Isolation Mocking External Services →

© 2026 hectoday. All rights reserved.