PLAINFINCH learn by building

Project: launch-ready server

You will be able to turn a working booking server into one that is safe and ready to put online.

What does launch-ready mean for this server?

The bike shop's booking server already works on a laptop. Customers can see free days and book a repair. Before strangers on the internet can reach it, it needs the habits from the last part of the course: it must run where the host puts it, give away nothing an attacker can use, refuse bad input, survive a script that hammers it, fail politely, and tell the host it is alive.

Each item below is a few lines. Work through them in order, sending requests before and after each change, and read the Requests view as you go.

  • Run on the host's port: process.env.PORT, with 3000 as the laptop fallback.
  • Send safe headers and stop advertising Express.
  • Cache static files for a day.
  • Validate every booking and answer 400 when it is wrong.
  • Limit bookings per visitor and answer 429 past the limit.
  • Add /health, a custom 404 and a custom error handler.

Which security headers should you set by hand?

Three headers do most of the work for a small server. X-Content-Type-Options: nosniff tells the browser to believe your Content-Type, so a text file can't be run as a script. Content-Security-Policy lists where a page may load scripts, styles and images from; default-src 'self' allows only your own origin, and frame-ancestors 'none' stops other sites showing your pages in a frame. Referrer-Policy: no-referrer keeps your URLs out of other sites' logs when visitors follow a link away. Once the site is on HTTPS, add Strict-Transport-Security so browsers never try plain HTTP again.

Set them in one middleware at the top, before express.static(). Static files are answered by express.static() itself, which never calls next() for a file it found, so middleware below it never sees those requests. Also call app.disable('x-powered-by'). Hiding the framework name doesn't make the server secure, but there is no reason to help someone looking for known weaknesses. Many teams install a package that sets a longer list of headers for them; writing the short list yourself first means you know what that package does.

app.disable('x-powered-by');

app.use((req, res, next) => {
  res.set({
    'X-Content-Type-Options': 'nosniff',
    'Content-Security-Policy': "default-src 'self'; frame-ancestors 'none'",
    'Referrer-Policy': 'no-referrer'
  });
  next();
});

app.use(express.static(path.join(__dirname, 'public'), { maxAge: 86400000 }));

How strict should booking validation be?

The starting code copies the whole request body into the booking with ...req.body. Anyone could store a blank name, a made-up bike, a day of next week, or extra fields such as "paid": true that the shop never asked for. Build the booking from named fields only, after checking each one.

Trim the name and require 1 to 60 characters. Compare bike against a fixed list. Check that day has the YYYY-MM-DD shape; a real shop would also check that it is a real date in the future and a day the workshop is open. Collect every problem in one array and answer 400 with all of them, so a form can show them together. Write return before that res.status(400), or the handler carries on and stores the bad booking anyway.

How does the booking rate limit work?

A Map remembers, for each visitor's IP address, how many bookings they made and when their current minute started. The middleware runs before the booking handler. A first request, or one after the minute has passed, starts a fresh window. Inside the window it counts, and past five it answers 429 Too Many Requests with Retry-After in seconds, the time left in the window.

Press **Book a service** six times and watch the sixth answer change. The practice clock moves one second per request, so all six land in the same minute. Press **Book a minute later** and the visitor can book again. On a real host, req.ip is only the visitor's address once trust proxy is set; otherwise every request seems to come from the proxy, and one busy minute would lock out everyone at once. The Map also lives in one process's memory, so it resets on restart and isn't shared between copies of the server.

const hits = new Map();
function limitBookings(req, res, next) {
  const now = Date.now();
  const entry = hits.get(req.ip);
  if (!entry || now - entry.start > 60000) {
    hits.set(req.ip, { count: 1, start: now });
    return next();
  }
  entry.count += 1;
  if (entry.count > 5) {
    res.set('Retry-After', String(Math.ceil((entry.start + 60000 - now) / 1000)));
    return res.status(429).json({ error: 'Too many bookings. Please try again in a minute.' });
  }
  next();
}

What should visitors see when something goes wrong?

Two handlers at the very end of the file decide that. The first is a plain middleware: any request that reaches it matched no route, so it answers 404. Under /api/ it sends JSON, which a script can read; everywhere else it sends a short page with a link home. The second has four arguments, (err, req, res, next), which is how Express knows it handles errors. It logs the full error for you and sends visitors a plain message.

Errors that carry their own status, such as 400 from express.json() when the body isn't valid JSON, should keep it. The handler in the finished code passes through statuses from 400 to 499 and turns everything else into 500. Better still, stop errors before they happen: the starting code's /api/bookings/:id reads booking.id on undefined for an unknown number, and a return res.status(404) fixes the cause instead of the symptom.

FigureThe launch-ready booking server from top to bottom: security headers, static files cached a day, the JSON parser, GET /health, the booking routes with a rate limit giving 429 and validation giving 400, then a 404 handler and an error handler giving 500.The launch-ready booking server from top to bottom: security headers, static files cached a day, the JSON parser, GET /health, the booking routes with a rate limit giving 429 and validation giving 400, then a 404 handler and an error handler giving 500.
Each launch habit is one entry in the list, and its place in the list counts: headers above express.static so files get them too, the limiter before validation on POST /api/bookings, and the 404 and error handlers at the very end.

How do caching, the port and /health fit in?

These come straight from the caching and hosting lessons. Give express.static() a maxAge of 86400000 milliseconds so returning visitors reuse the stylesheet for a day. Leave the API answers alone: bookings change, and a list of free days that is a minute old can mean a double booking.

Read the port with process.env.PORT || 3000 and log it at startup; this practice host sets 8080, so the Log should say so. Add GET /health above the 404 handler, answering 200 with { "status": "ok" }. Set trust proxy to the number of proxies in front of you, which is one on most hosts.

How do you test the whole server before launch?

Go through the Try row as a stranger would. Read the headers on GET / and GET /style.css and confirm both carry nosniff and the policy, and neither says X-Powered-By. Send **Bad booking** and confirm the 400, then GET /api/bookings to confirm nothing was stored. Book six times and check the 429. Ask for /prices and /api/bookings/99 and read what a visitor would see. Finally, open the Log and make sure it holds the details the visitor didn't get, and nothing secret.

What should you check when an AI tool writes this for you?

Assistants are good at producing all of these pieces and bad at putting them in the right order. Look for header middleware placed after express.static(), a 404 handler above routes added later, an error handler with three arguments, validation that sends 400 without return, a rate limiter whose window never resets or is keyed on something the client chooses, trust proxy set to true, and ...req.body spread straight into stored data. Each of those passes a quick test by hand and fails on a real day. Run the checks again after every change the tool makes.

Field notes

  • Before strangers can reach a server, it needs safe headers, checks and clear error pages.
  • Headers like X-Content-Type-Options: nosniff and a Content-Security-Policy go on every response.
  • Check each booking field instead of copying the whole request body.
  • End the file with a 404 handler and an error handler that hides details from visitors.

Easy to get wrong

express.static() answers the request when it finds a file and never calls next(), so headers set by later middleware never reach your HTML, CSS and images.

Spreading the body stores any field a client sends, including ones your code later trusts. Build stored objects from fields you checked.

Sending a response doesn't stop the function. Without return, the code below still runs, stores the bad booking and tries to answer twice.

Express recognises an error handler by its four parameters. With three, it is treated as ordinary middleware and never receives errors.

Questions people ask

Should I use a package for security headers instead of writing them?

For a real launch, a well-maintained package that sets a full list of headers is a good choice. Writing the main ones by hand first means you can read what it sends and adjust the policy when a page needs something extra.

Is a rate limit in memory good enough?

For one small server, yes. It resets on restart and isn't shared between several copies of the server, so larger sites keep the counters in a shared store or let the platform's edge do the limiting.

Why does the Content-Security-Policy not break the page here?

The page only loads /style.css from the same origin, which default-src 'self' allows. Inline scripts, inline style attributes and files from other sites would be blocked, which is what you should test when you add them.

What else would a real bike shop need before launch?

A database instead of an array, logins for staff who see customer names, HTTPS with Strict-Transport-Security, backups, and a way to cancel or change a booking. Each of those uses a lesson from this course.

Why is the starting code allowed to crash on /api/bookings/99?

It shows what launches look like without the last checks: Express catches the TypeError, logs it and answers 500 with its own page. Your job is to make that answer deliberate.

Keep going