Project setup
Let’s build something
In the last lesson, we talked about why API design matters. Now it’s time to set up the project we’ll be building throughout this course: a bookstore API.
We need a working server, some data to work with, and a foundation we can build on as we add features lesson by lesson. By the end of this lesson, you’ll have a running API with a health check endpoint and a data store full of books, authors, and reviews.
Create the project
mkdir bookstore-api
cd bookstore-api
npm init -y
npm install @hectoday/http zod srvx
npm install -D typescript @types/node tsx Let’s walk through what each package does:
@hectoday/httpis our routing library. It handles incoming requests and maps them to handler functions.zodis a validation library. We’ll use it later to validate request bodies, making sure clients send the right data.srvxis a lightweight HTTP server. It takes our app and starts listening for requests.tsxlets us run TypeScript files directly without a separate compile step. We’ll use it for development.
Now create the TypeScript configuration file, tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src"]
} The data store
Every API needs somewhere to store data. We’re going to keep things simple and use plain TypeScript arrays. No database to install, no connection strings, no setup. Just arrays that live in memory.
Create src/db.ts:
// src/db.ts
export interface Author {
id: string;
name: string;
bio: string;
}
export interface Book {
id: string;
title: string;
isbn?: string;
genre: string;
publishedAt?: string;
authorId: string;
createdAt: string;
}
export interface Review {
id: string;
bookId: string;
reviewerName: string;
rating: number;
body?: string;
createdAt: string;
}
export const authors: Author[] = [];
export const books: Book[] = [];
export const reviews: Review[] = []; Let’s break down what’s happening here.
We define three TypeScript interfaces that describe the shape of our data. Then we create three arrays, one for each type of record in our bookstore.
Author represents the people who write books. Each author has an id, a name, and a bio.
Book represents the books themselves. Each book has a title, an optional isbn (International Standard Book Number), a genre, an optional publishedAt date, an authorId that references one of our authors, and a createdAt timestamp. That authorId field is important. It tells us which author wrote the book.
Review represents reader reviews. Each review has a bookId (linking it to a specific book), a reviewerName, a rating between 1 and 5, and an optional body for the review text.
Notice the relationships: an author has many books, and a book has many reviews. This is a classic one-to-many pattern. We’ll explore how these relationships affect our API design in the resource modeling lesson later.
Why does the Book interface have authorId instead of the Author interface having a list of book IDs? Because it mirrors how real-world data relationships work. In a one-to-many relationship, the “many” side holds the reference. One author can have many books, so each book points back to its author. When you want to find all books by a specific author, you filter the books array by authorId.
Seed data
An empty data store isn’t very useful for testing. Let’s add some books, authors, and reviews so we have something to work with right away. Add this to the bottom of src/db.ts:
// Seed data
authors.push(
{ id: "author-1", name: "Ernest Hemingway", bio: "American novelist and journalist." },
{
id: "author-2",
name: "Ursula K. Le Guin",
bio: "American author of science fiction and fantasy.",
},
{
id: "author-3",
name: "Chimamanda Ngozi Adichie",
bio: "Nigerian writer of novels and short stories.",
},
);
books.push(
{
id: "book-1",
title: "The Old Man and the Sea",
isbn: "978-0684801223",
genre: "fiction",
publishedAt: "1952-09-01",
authorId: "author-1",
createdAt: "2024-01-01T00:00:00Z",
},
{
id: "book-2",
title: "A Farewell to Arms",
isbn: "978-0684801469",
genre: "fiction",
publishedAt: "1929-09-27",
authorId: "author-1",
createdAt: "2024-01-02T00:00:00Z",
},
{
id: "book-3",
title: "The Left Hand of Darkness",
isbn: "978-0441478125",
genre: "science-fiction",
publishedAt: "1969-03-01",
authorId: "author-2",
createdAt: "2024-01-03T00:00:00Z",
},
{
id: "book-4",
title: "A Wizard of Earthsea",
isbn: "978-0547722023",
genre: "fantasy",
publishedAt: "1968-11-01",
authorId: "author-2",
createdAt: "2024-01-04T00:00:00Z",
},
{
id: "book-5",
title: "Americanah",
isbn: "978-0307455925",
genre: "fiction",
publishedAt: "2013-05-14",
authorId: "author-3",
createdAt: "2024-01-05T00:00:00Z",
},
{
id: "book-6",
title: "Half of a Yellow Sun",
isbn: "978-1400095209",
genre: "historical-fiction",
publishedAt: "2006-09-12",
authorId: "author-3",
createdAt: "2024-01-06T00:00:00Z",
},
);
reviews.push(
{
id: "review-1",
bookId: "book-1",
reviewerName: "Alice",
rating: 5,
body: "A masterpiece of simplicity.",
createdAt: "2024-01-15T10:00:00Z",
},
{
id: "review-2",
bookId: "book-1",
reviewerName: "Bob",
rating: 4,
body: "Beautiful and sparse.",
createdAt: "2024-02-20T14:30:00Z",
},
{
id: "review-3",
bookId: "book-3",
reviewerName: "Carol",
rating: 5,
body: "Groundbreaking exploration of gender.",
createdAt: "2024-03-10T09:00:00Z",
},
{
id: "review-4",
bookId: "book-5",
reviewerName: "Dave",
rating: 5,
body: "Changed how I think about identity.",
createdAt: "2024-04-05T16:45:00Z",
},
); Because the data lives in memory, it gets re-created from scratch every time the server starts. This is actually a nice feature during development. You can experiment freely, add or delete records, and restart the server to get back to a clean state. No need to worry about cleaning up test data.
We now have six books across three authors, plus four reviews. That’s enough data to demonstrate pagination, filtering by genre, sorting by date, and nested resource queries.
The app shell and server
Now let’s create the actual application. We’ll start with the simplest possible setup: a single health check endpoint.
// src/app.ts
import { setup, route } from "@hectoday/http";
export const app = setup({
routes: [route.get("/health", { resolve: () => Response.json({ status: "ok" }) })],
}); This creates an app with one route. When someone sends a GET request to /health, the server responds with { "status": "ok" }. That’s it. We’ll add real routes in the next lesson.
Now the server file that actually starts listening for requests:
// src/server.ts
import { serve } from "srvx";
import { app } from "./app.js";
serve({ fetch: app.fetch, port: 3000 }); The serve function takes our app’s fetch handler and starts an HTTP server on port 3000.
One last thing. Add "type": "module" and a dev script to your package.json:
{
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts"
}
} The tsx watch command runs our server and automatically restarts it whenever we change a file. Very handy during development.
Try it out
npm run dev
curl http://localhost:3000/health
# { "status": "ok" } If you see { "status": "ok" }, the server is running and ready to go. The seed data is loaded in memory and ready for the routes we’ll build in the next lesson.
What’s next
We have a running server and a data store full of data, but no real endpoints yet. In the next lesson, we’ll start building actual routes. And we’ll talk about the single most important rule of REST API design: URLs should describe resources, not actions.
Exercises
Exercise 1: Run the app and verify the health check endpoint returns { "status": "ok" }.
Exercise 2: Look at the interfaces carefully. What is the relationship between authors and books? Between books and reviews? Both are one-to-many.
Exercise 3: Why does the Book interface have authorId rather than the Author interface having a list of book IDs? Think about how one-to-many relationships are typically represented when each record is a plain object.
Why do we use a separate array for reviews instead of storing reviews as a JSON array on the book?