Testing

The app.request() method lets you test routes without a running server. Pass a path and optional options, get a Response back.

Basic testing

import { describe, test, expect } from "vitest";
import { app } from "../server";

describe("GET /books", () => {
  test("returns a list of books", async () => {
    const res = await app.request("/books");
    const body = await res.json();

    expect(res.status).toBe(200);
    expect(Array.isArray(body)).toBe(true);
  });
});

No HTTP server is started. app.request processes the request through the full lifecycle: onRequest, route matching, validation, handler, onResponse.

Testing POST requests

test("creates a book", async () => {
  const res = await app.request("/books", {
    method: "POST",
    body: {
      title: "Kindred",
      authorId: "author-1",
      genre: "science-fiction",
    },
  });

  expect(res.status).toBe(201);
  const book = (await res.json()) as { title: string };
  expect(book.title).toBe("Kindred");
});

When you pass a body, it is automatically serialized as JSON and the Content-Type header is set to application/json.

Testing with authentication

Include the appropriate headers in the options:

test("authenticated request", async () => {
  const res = await app.request("/profile", {
    headers: { Authorization: "Bearer valid-test-token" },
  });
  expect(res.status).toBe(200);
});

test("unauthenticated request", async () => {
  const res = await app.request("/profile");
  expect(res.status).toBe(401);
});

Testing validation errors

test("returns 400 for invalid body", async () => {
  const res = await app.request("/books", {
    method: "POST",
    body: {}, // Missing required fields
  });

  expect(res.status).toBe(400);
  const body = (await res.json()) as { error: unknown };
  expect(body.error).toBeDefined();
});

Testing error handling

test("returns 404 for unknown book", async () => {
  const res = await app.request("/books/nonexistent-id");
  expect(res.status).toBe(404);
});

Testing response headers

test("includes request ID in response", async () => {
  const res = await app.request("/books");
  expect(res.headers.get("x-request-id")).toBeDefined();
});

test("includes CORS headers", async () => {
  const res = await app.request("/books", {
    headers: { Origin: "https://myapp.com" },
  });
  expect(res.headers.get("access-control-allow-origin")).toBe("https://myapp.com");
});

Testing with query parameters

test("filters by tag", async () => {
  const res = await app.request("/books", {
    headers: { Authorization: "Bearer alice" },
    query: { tag: "fiction", limit: "10" },
  });
  expect(res.status).toBe(200);
});

RequestOptions

interface RequestOptions {
  method?: string; // Default: "GET"
  body?: unknown; // Automatically serialized as JSON
  headers?: Record<string, string>;
  query?: Record<string, string | string[]>;
}