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.