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 GET Endpoints

The GET testing pattern

Every GET test follows the same pattern: seed known data, make a request, assert the response matches expectations.

Testing a list endpoint

describe("GET /v2/books", () => {
  beforeEach(() => {
    testDb.exec("DELETE FROM reviews; DELETE FROM books; DELETE FROM authors;");
    testDb.prepare("INSERT INTO authors (id, name) VALUES (?, ?)").run("a1", "Butler");
    testDb.prepare("INSERT INTO authors (id, name) VALUES (?, ?)").run("a2", "Borges");
    testDb
      .prepare("INSERT INTO books (id, title, author_id, genre) VALUES (?, ?, ?, ?)")
      .run("b1", "Kindred", "a1", "science-fiction");
    testDb
      .prepare("INSERT INTO books (id, title, author_id, genre) VALUES (?, ?, ?, ?)")
      .run("b2", "Ficciones", "a2", "fiction");
  });

  test("returns all 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(2);
  });

  test("books are ordered by title", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books"));
    const data = await response.json();

    expect(data[0].title).toBe("Ficciones");
    expect(data[1].title).toBe("Kindred");
  });

  test("each book has the v2 response shape", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books"));
    const data = await response.json();
    const book = data[0];

    expect(book.id).toBeDefined();
    expect(book.title).toBeDefined();
    expect(book.author).toBeDefined();
    expect(book.author.id).toBeDefined();
    expect(book.author.name).toBeDefined();
    expect(book.genre).toBeDefined();
    expect(book.createdAt).toBeDefined();
    // v1 fields should NOT be present
    expect(book.author_name).toBeUndefined();
    expect(book.created_at).toBeUndefined();
  });
});

Testing query parameters

describe("GET /v2/books with query params", () => {
  test("filters by genre", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books?genre=fiction"));
    const data = await response.json();

    expect(data).toHaveLength(1);
    expect(data[0].genre).toBe("fiction");
  });

  test("returns empty array for genre with no books", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books?genre=fantasy"));
    const data = await response.json();

    expect(data).toHaveLength(0);
  });
});

Testing a single-resource endpoint

describe("GET /v2/books/:id", () => {
  test("returns the book with correct shape", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books/b1"));
    const book = await response.json();

    expect(response.status).toBe(200);
    expect(book.id).toBe("b1");
    expect(book.title).toBe("Kindred");
    expect(book.author).toEqual({ id: "a1", name: "Butler" });
  });

  test("returns 404 for nonexistent book", async () => {
    const response = await app.fetch(new Request("http://localhost/v2/books/nonexistent"));

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

  test("includes ratings", async () => {
    testDb
      .prepare("INSERT INTO reviews (id, book_id, user_id, rating) VALUES (?, ?, ?, ?)")
      .run("r1", "b1", "u1", 5);
    testDb
      .prepare("INSERT INTO reviews (id, book_id, user_id, rating) VALUES (?, ?, ?, ?)")
      .run("r2", "b1", "u2", 3);

    const response = await app.fetch(new Request("http://localhost/v2/books/b1"));
    const book = await response.json();

    expect(book.ratings.average).toBe(4);
    expect(book.ratings.count).toBe(2);
  });
});

Notice how the ratings test seeds reviews in the test itself — not in beforeEach. This keeps each test self-contained: the rating test adds reviews because it is testing ratings. Other tests do not need reviews.

What to assert

Status code. Always check it first. A 500 with the right body shape is still a bug.

Response shape. Check that expected fields exist and unexpected fields do not (v1 fields on v2 endpoints).

Data correctness. Check that values match what was seeded. Title is “Kindred”, not “Ficciones”.

Edge cases. Empty results, not found, no reviews (null rating).

Exercises

Exercise 1: Write tests for a list endpoint. Seed 3 items. Check count, order, and response shape.

Exercise 2: Write tests for a detail endpoint. Test found (200), not found (404), and with related data (reviews/ratings).

Exercise 3: Test query parameter filtering. Seed books in multiple genres. Verify filtering returns only matching books.

Why seed data in beforeEach instead of relying on data from previous tests?

← Testing Route Handlers Testing POST Endpoints →

© 2026 hectoday. All rights reserved.