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

Testing Route Handlers

Integration tests for APIs

Unit tests test individual functions. Integration tests test the full request-response cycle: the client sends a request, the route handler processes it (validates input, queries the database, formats the response), and returns a response with the correct status code, headers, and body.

app.fetch(): no server needed

Hectoday HTTP’s setup() returns an app with a fetch method. This method accepts a Request and returns a Response — the same interface as a running server, but without starting one.

import { app } from "../../src/app.js";

test("GET /v2/books returns 200", async () => {
  const request = new Request("http://localhost/v2/books");
  const response = await app.fetch(request);

  expect(response.status).toBe(200);
});

No server port. No network. No listen(). The test calls app.fetch() directly — fast and deterministic.

Parsing the response

test("GET /v2/books returns JSON array", async () => {
  const response = await app.fetch(new Request("http://localhost/v2/books"));
  const data = await response.json();

  expect(response.status).toBe(200);
  expect(response.headers.get("content-type")).toContain("application/json");
  expect(Array.isArray(data)).toBe(true);
});

Check the status code, headers, and body. Parse JSON with response.json(). Assert against the parsed data.

Building requests

For POST, PUT, PATCH — you need to build requests with bodies and headers:

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,
  });
}

// Usage
const request = jsonRequest("POST", "/v2/books", {
  title: "Kindred",
  authorId: "550e8400-e29b-41d4-a716-446655440000",
  genre: "science-fiction",
});
const response = await app.fetch(request);

The http://localhost base URL is required by the Request constructor but is not actually contacted — app.fetch processes the request directly.

A complete integration test

import { describe, test, expect, beforeEach } from "vitest";
import { app } from "../../src/app.js";
import { testDb } from "../setup.js";

beforeEach(() => {
  testDb.exec("DELETE FROM reviews; DELETE FROM books; DELETE FROM authors;");
  testDb.prepare("INSERT INTO authors (id, name) VALUES (?, ?)").run("author-1", "Octavia Butler");
  testDb
    .prepare("INSERT INTO books (id, title, author_id, genre) VALUES (?, ?, ?, ?)")
    .run("book-1", "Kindred", "author-1", "science-fiction");
});

describe("GET /v2/books", () => {
  test("returns 200 with list of books", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books"));
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveLength(1);
    expect(data[0].title).toBe("Kindred");
    expect(data[0].author.name).toBe("Octavia Butler");
  });

  test("returns empty array when no books", async () => {
    testDb.exec("DELETE FROM books");
    const response = await app.fetch(new Request("http://localhost/v2/books"));
    const data = await response.json();

    expect(response.status).toBe(200);
    expect(data).toHaveLength(0);
  });
});

Seed data → make request → assert response. The test verifies the entire pipeline: routing, query, transformation, response formatting.

Exercises

Exercise 1: Write an integration test for GET /v2/books. Seed 3 books. Verify all 3 appear in the response.

Exercise 2: Write an integration test for GET /v2/books/:id. Verify it returns the correct book with nested author.

Exercise 3: Create the jsonRequest helper. Use it to test a POST endpoint.

Why use app.fetch() instead of starting a real server for integration tests?

← Testing Business Logic Testing GET Endpoints →

© 2026 hectoday. All rights reserved.