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
Your mission, step by step
- Add a logging middleware that prints LOG <method> <url> for every request.
- Build a validated POST /books : title required (string), year optional (number), else 400 .
- Add POST /login that signs a JWT for a demo user and returns { token } .
- Write a JWT auth middleware that reads the Bearer header and rejects missing/invalid tokens with 401 .
- Protect GET /books with that middleware and return { user, books } .
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.
Pro Tips
- 💡 Validate at the edge: reject bad input as early as possible with a clear 400 so the rest of your handler can trust the data.
- 💡 One reusable auth middleware: write it once and drop it in front of every route that needs a logged-in user.
- 💡 Layer your logs: morgan for the access log, pino for app events — both as JSON so they ship to one place.
Frequently Asked Questions
🎉 Checkpoint Complete!
- ✅ You combined validation, JWT auth, and logging into one Express API
- ✅ Validation failures return 400 ; missing/invalid tokens return 401
- ✅ The auth middleware reads the Bearer header and guards the protected route
- ✅ A request logger records every method and URL
- ✅ Swapping the array for a Mongoose model makes the data persistent
- ✅ You reviewed streams, routers, validation, uploads, logging, mongoose, and deployment
- ✅ Next lesson: Capstone Project — bring it all together into one app
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.
Continue this course
- Previous: Deploying Node
- Next: Capstone Project