Checkpoint: Build a REST API

This checkpoint ties the course together by having you build one small Express REST API that validates input, protects a route with a JWT, and logs every request — the same layers that power real production services.

Learn Checkpoint: Build a REST API in our free Node.js course — a beginner-friendly interactive lesson with worked examples, a practice exercise and a quick…

Part of the free Node.js course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

First you'll review everything you've learned, then take on a multi-step build challenge with a starter and a full solution, and finally test your understanding with a short checkpoint quiz before moving on to the capstone.

What You've Learned So Far

🔍 Warm-Up: the Core Logic, Running

Before the full build, here are the two ideas at the heart of the API — input validation and token issue/verify — as a small runnable program (no server needed), so you can confirm the logic with real output:

🛠️ Build Challenge: A Validated, Authenticated, Logged API

Here's a complete, working implementation. The comment at the bottom shows the exact output produced by hitting the routes in order — a created book, a rejected bad book, a blocked unauthenticated GET, a login, and finally an authorized GET:

📝 Checkpoint Quiz

Test yourself. Think through each answer first, then expand it to check.

Because middleware runs in order. express.json() parses the request body into req.body . If a route runs before it, req.body is still undefined and validation breaks.

400 Bad Request for invalid input (missing title, wrong type) and 401 Unauthorized for a missing or invalid token. They tell the client whether to fix the data or to authenticate.

Between the path and the handler: app.get('/books', auth, handler) . Express runs auth first, and it can send a 401 (and skip the handler) when the token is missing or invalid.

It reads the Authorization header, splits off the "Bearer " prefix, and passes the remaining token to jwt.verify(token, SECRET) , attaching the decoded claims to req.user .

In-memory data is lost on every restart and isn't shared across instances. Replace the array with a Mongoose model — await Book.create(...) and await Book.find() — to persist the data in MongoDB.

morgan as middleware logs every HTTP request automatically; pino writes your own structured app events. Piping morgan into pino gives you one consistent JSON format.

Practice quiz

Why must express.json() run before any route that reads req.body?

  • Because routes are slower than middleware
  • Because express.json() also handles authentication
  • Because middleware runs top to bottom, and express.json() is what parses the body into req.body
  • It does not matter where express.json() is placed

Answer: Because middleware runs top to bottom, and express.json() is what parses the body into req.body. Middleware runs in order. If a route runs before express.json(), req.body is still undefined and validation breaks.

What status code should a failed input validation return in this API?

  • 400 Bad Request
  • 200 OK
  • 401 Unauthorized
  • 500 Internal Server Error

Answer: 400 Bad Request. 400 Bad Request signals the client sent malformed or invalid data — here a missing title or a non-numeric year.

A request arrives with no token. Which status code does the auth middleware return?

  • 400 Bad Request
  • 403 Forbidden
  • 404 Not Found
  • 401 Unauthorized

Answer: 401 Unauthorized. 401 Unauthorized means the client is not authenticated — no token, or an invalid/expired one — so it must log in first.

Where does the auth middleware go for a protected route like GET /books?

  • Before express.json() at the top of the app
  • Between the path and the handler: app.get('/books', auth, handler)
  • Inside the route handler as the last line
  • It must be the very first app.use() call

Answer: Between the path and the handler: app.get('/books', auth, handler). Placed between the path and the handler, Express runs auth first and it can send a 401 and skip the handler when the token is bad.

How does the auth middleware extract the token from the request?

  • It reads the Authorization header, strips the 'Bearer ' prefix, and verifies the rest
  • It reads req.body.token
  • It reads a cookie named jwt
  • It reads req.query.token

Answer: It reads the Authorization header, strips the 'Bearer ' prefix, and verifies the rest. The middleware splits the Authorization header on a space, checks for the Bearer scheme, then passes the token to jwt.verify(token, SECRET).

The books are stored in an in-memory array. What is the main downside?

  • Arrays are slower than databases for any size
  • Arrays cannot store objects
  • The data is lost on every restart and is not shared across instances
  • Express cannot return arrays as JSON

Answer: The data is lost on every restart and is not shared across instances. In-memory data vanishes on restart and is not shared between processes; a Mongoose model persists it in MongoDB instead.

After a successful POST /books, which status code does the handler send?

  • 200 OK
  • 201 Created
  • 204 No Content
  • 202 Accepted

Answer: 201 Created. 201 Created indicates a new resource was created and is returned in the response body, which is exactly what the route does.

Which logger records every incoming HTTP request automatically as middleware?

  • pino
  • winston-file
  • console.trace
  • morgan

Answer: morgan. morgan plugs in as middleware and logs every HTTP request; pino is better suited to your own structured application events.

What does POST /login return to the client in this checkpoint?

  • A redirect to /books
  • A signed JWT, e.g. { token }
  • The full user record with a password
  • A session cookie only

Answer: A signed JWT, e.g. { token }. The login route signs a JWT with jwt.sign({ sub: 1, name: 'reader' }, SECRET) and returns it as { token }.

How would you make the in-memory books persistent with a real database?

  • Write the array to a global variable
  • Store the books in process.env
  • Replace the array with a Mongoose model: await Book.create(...) and await Book.find()
  • Increase the array size limit

Answer: Replace the array with a Mongoose model: await Book.create(...) and await Book.find(). Swapping books.push for await Book.create and the array read for await Book.find persists data in MongoDB while validation, JWT, and logging stay the same.