Project Setup
The starting point
The book catalog API from previous courses — books, authors, reviews. Currently using console.log everywhere. By the end of this course, every log will be structured JSON with context.
Create the project
mkdir observable-catalog
cd observable-catalog
npm init -y
npm install @hectoday/http zod srvx better-sqlite3
npm install -D typescript @types/node @types/better-sqlite3 tsx Create tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src"]
} Add "type": "module" and a dev script to package.json:
{
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts"
}
} To start the server:
npm run dev The current state (console.log everywhere)
// src/app.ts — BEFORE (unstructured logging)
import { setup, route } from "@hectoday/http";
import db from "./db.js";
export const app = setup({
onRequest: ({ request }) => {
console.log("Request:", request.method, new URL(request.url).pathname);
return {};
},
onError: ({ error }) => {
console.error("Error:", error.message);
return Response.json({ error: "Internal error" }, { status: 500 });
},
routes: [
route.get("/books", {
resolve: () => {
console.log("Fetching books...");
const books = db.prepare("SELECT * FROM books").all();
console.log("Found", books.length, "books");
return Response.json(books);
},
}),
],
}); The output:
Request: GET /books
Fetching books...
Found 5 books
Request: POST /books
Error: UNIQUE constraint failed
Request: GET /books/nonexistent No timestamps, no levels, no request IDs, no searchable structure. This is what we are replacing.
The database (same as previous courses)
// src/db.ts
import Database from "better-sqlite3";
const db = new Database("catalog.db");
db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");
db.exec(`
CREATE TABLE IF NOT EXISTS authors (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
bio TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS 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')),
FOREIGN KEY (author_id) REFERENCES authors(id)
);
CREATE TABLE IF NOT EXISTS 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 default db; The goal
By the end of this course, the same request produces:
{
"timestamp": "2024-01-15T14:32:05.123Z",
"level": "info",
"message": "request completed",
"requestId": "req_a1b2c3",
"method": "GET",
"path": "/books",
"status": 200,
"duration": 12
} Structured. Timestamped. Searchable. Every log entry tied to a request with a unique ID.
Exercises
Exercise 1: Start the server. Make 10 requests. Look at the console output. Try to find a specific request.
Exercise 2: Count every console.log and console.error in the project. These will all be replaced.
Exercise 3: Write down what information you wish each log entry had (timestamp, user, request ID, etc.).
What is the first problem to solve when replacing console.log?