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

The POST testing pattern

POST tests verify: the resource is created, the response is 201 with the created resource, the Location header points to the new resource, and the data is persisted in the database.

Testing successful creation

describe("POST /v2/books", () => {
  beforeEach(() => {
    testDb.exec("DELETE FROM books; DELETE FROM authors;");
    testDb.prepare("INSERT INTO authors (id, name) VALUES (?, ?)").run("a1", "Butler");
  });

  test("creates a book and returns 201", async () => {
    const response = await app.fetch(
      jsonRequest("POST", "/v2/books", {
        title: "Kindred",
        authorId: "a1",
        genre: "science-fiction",
      }),
    );
    const book = await response.json();

    expect(response.status).toBe(201);
    expect(book.title).toBe("Kindred");
    expect(book.id).toBeDefined();
  });

  test("returns Location header", async () => {
    const response = await app.fetch(
      jsonRequest("POST", "/v2/books", {
        title: "Kindred",
        authorId: "a1",
        genre: "science-fiction",
      }),
    );
    const book = await response.json();

    expect(response.headers.get("location")).toBe(`/v2/books/${book.id}`);
  });

  test("book is persisted in the database", async () => {
    await app.fetch(
      jsonRequest("POST", "/v2/books", {
        title: "Kindred",
        authorId: "a1",
        genre: "science-fiction",
      }),
    );

    const books = testDb.prepare("SELECT * FROM books").all();
    expect(books).toHaveLength(1);
    expect((books[0] as any).title).toBe("Kindred");
  });

  test("created book appears in GET /v2/books", async () => {
    await app.fetch(
      jsonRequest("POST", "/v2/books", {
        title: "Kindred",
        authorId: "a1",
        genre: "science-fiction",
      }),
    );

    const listResponse = await app.fetch(new Request("http://localhost/v2/books"));
    const data = await listResponse.json();

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

The last test is a true integration test: create via POST, verify via GET. This catches bugs where the creation succeeds but the query does not return the new resource (wrong query, missing JOIN, etc.).

Testing with optional fields

test("creates book with description", async () => {
  const response = await app.fetch(
    jsonRequest("POST", "/v2/books", {
      title: "Kindred",
      authorId: "a1",
      genre: "science-fiction",
      description: "A novel about time travel",
    }),
  );
  const book = await response.json();

  expect(response.status).toBe(201);
  expect(book.description).toBe("A novel about time travel");
});

test("creates book without optional description", async () => {
  const response = await app.fetch(
    jsonRequest("POST", "/v2/books", {
      title: "Kindred",
      authorId: "a1",
      genre: "science-fiction",
    }),
  );
  const book = await response.json();

  expect(response.status).toBe(201);
  expect(book.description).toBeNull();
});

Testing validation failures

test("returns 400 for missing title", async () => {
  const response = await app.fetch(
    jsonRequest("POST", "/v2/books", {
      authorId: "a1",
      genre: "science-fiction",
    }),
  );

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

test("returns 400 for invalid genre", async () => {
  const response = await app.fetch(
    jsonRequest("POST", "/v2/books", {
      title: "Kindred",
      authorId: "a1",
      genre: "romance",
    }),
  );

  expect(response.status).toBe(400);
  const error = await response.json();
  expect(error.error).toBeDefined();
});

[!NOTE] Testing validation failures here complements the schema unit tests from the previous section. Schema tests check that the schema rejects invalid input. Integration tests check that the route handler returns the correct HTTP status code and error format when the schema rejects input.

Exercises

Exercise 1: Test POST creation: 201 status, correct body, Location header, persisted in database.

Exercise 2: Test POST with and without optional fields. Verify both succeed with correct data.

Exercise 3: Test POST with invalid input. Verify 400 status code and error response.

Why test that the created resource appears in GET after POST?

← Testing GET Endpoints Testing Error Responses →

© 2026 hectoday. All rights reserved.