Project Setup
The testing stack
This course uses Vitest — a fast, TypeScript-native test runner. It runs tests in parallel, supports ESM imports, and has a familiar API (compatible with Jest).
Adding Vitest
Start with the book catalog API from the Caching or API Versioning course:
npm install -D vitest Add a test script to package.json:
{
"scripts": {
"dev": "tsx watch src/server.ts",
"test": "vitest run",
"test:watch": "vitest"
}
} vitest run executes all tests once (for CI). vitest runs in watch mode (re-runs on file changes).
Vitest configuration
Create vitest.config.ts:
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node",
include: ["tests/**/*.test.ts"],
setupFiles: ["tests/setup.ts"],
},
}); globals: true makes describe, test, expect available without importing (optional — you can import them explicitly). setupFiles runs before all tests — used for database setup.
Test file structure
tests/
setup.ts # Global setup (test database)
unit/
transformers.test.ts # Unit tests for response transformers
schemas.test.ts # Unit tests for Zod schemas
helpers.test.ts # Unit tests for helper functions
integration/
books.test.ts # Integration tests for book endpoints
reviews.test.ts # Integration tests for review endpoints
errors.test.ts # Integration tests for error responses
helpers/
factories.ts # Test data factories
request.ts # Request helper functions Unit tests in tests/unit/. Integration tests in tests/integration/. Test helpers in tests/helpers/.
The test database
Tests should not use the production database. Create a separate test database:
// tests/setup.ts
import Database from "better-sqlite3";
// Use an in-memory database for tests — fast and isolated
const testDb = new Database(":memory:");
testDb.pragma("journal_mode = WAL");
testDb.pragma("foreign_keys = ON");
// Create the schema (same as production)
testDb.exec(`
CREATE TABLE authors (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE books (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
author_id TEXT NOT NULL,
genre TEXT NOT NULL,
description TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (author_id) REFERENCES authors(id)
);
CREATE TABLE reviews (
id TEXT PRIMARY KEY,
book_id TEXT NOT NULL,
user_id TEXT NOT NULL,
rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 5),
body TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY (book_id) REFERENCES books(id)
);
`);
export { testDb }; [!NOTE] SQLite’s
:memory:database exists only in RAM. It is fast (no disk I/O), isolated (each test run starts fresh), and disposable (gone when the process ends). The Database Design course covered in-memory databases for this exact use case.
Your first test
// tests/unit/transformers.test.ts
import { describe, test, expect } from "vitest";
describe("example", () => {
test("1 + 1 equals 2", () => {
expect(1 + 1).toBe(2);
});
}); npm test
# ✓ tests/unit/transformers.test.ts (1 test)
# Test Files 1 passed (1)
# Tests 1 passed (1) The test runner finds files matching tests/**/*.test.ts, runs them, and reports results. Green means pass. Red means fail.
Exercises
Exercise 1: Install Vitest. Create vitest.config.ts. Write and run a trivial test.
Exercise 2: Create the test database setup file with an in-memory SQLite database.
Exercise 3: Create the test directory structure: unit/, integration/, helpers/.
Why use an in-memory database for tests instead of the production database?