See inside it: logs, errors and timings
Continues from the last build: CI runs unit, integration and the money-path e2e on every PR.
A client emails you Monday morning: "my invoice never sent, this is the third time." You ask when.
What you'll build
You can answer "what happened to this specific invoice send yesterday around lunch" in under two minutes: grep the requestId, see the full request-to-job timeline as structured JSON, see the exact query that was slow, see the Sentry issue if one was thrown, and know within seconds whether Postgres, Redis or the worker was the actual cause.
See how we teach, before you sign up
You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:
import pino from 'pino';
export const baseLogger = pino({
level: process.env.LOG_LEVEL ?? 'info',
transport:
process.env.NODE_ENV === 'production'
? undefined
: { target: 'pino-pretty', options: { colorize: true } },
});
export function requestLogger(requestId: string, orgId: string | null, route: string) {
return baseLogger.child({ requestId, orgId, route });
}
Reading this file
level: process.env.LOG_LEVEL ?? 'info'Log level is env-configurable, set to debug locally to see per-query timing, leave at info in production.target: 'pino-pretty', options: { colorize: true }Human-readable output only in dev, production always emits raw JSON lines.return baseLogger.child({ requestId, orgId, route })This child logger factory is what every route handler and the worker call to get a logger pre-tagged with the request's identity.
Starter file for this rung.
That's 1 of 9 explained code blocks in this single project.
The build, milestone by milestone
- 1
Structured logs with request IDs
5 guided stepsThe client's "it failed around lunch" is useless without a way to isolate one request. A requestId that travels from HTTP request into the job payload turns a wall of scrolling text into one JSON line per event you can grep and pipe to jq.
- 2
Track errors with Sentry
5 guided stepsA stack trace with minified variable names and no source map tells you nothing at 11pm. And if every 401 from an expired session shows up as a Sentry alert, you will start ignoring Sentry, which defeats the point.
- 3
Time the queries and the routes
5 guided steps"It feels slow" is not a number. A route that takes 640ms because of an N+1 query looks identical to a route that takes 640ms because Stripe's API is slow, until you have durationMs on the route AND on every query inside it.
- 4
A /healthz that means something
5 guided stepsA deploy platform restarts or routes traffic based on health checks. A /healthz that always returns 200 tells the platform to keep sending traffic to an instance whose database connection is dead, which is worse than no health check at all.
- 5
Alert on what matters
5 guided stepsWatching individual causes (a specific query, a specific 3rd party API) means writing an alert for every new thing that can break. Watching symptoms (error rate, latency, queue depth, failed jobs) catches every cause including ones you have not thought of yet, because they all eventually show up as one of these four numbers moving.
What's inside when you start
You'll walk away with
This is portfolio-grade. Build it free.
Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.
Start building