Building a REST API with Express

intermediate
16 min

Building a REST API with Express

You now know routing, middleware, body parsing, routers, and MongoDB. This lesson assembles those skills into a complete REST API for a bookstore, following conventions that professional teams expect: predictable URLs, correct status codes, pagination, and consistent response shapes.

REST in Two Minutes

REST treats your data as resources addressed by nouns, with HTTP methods expressing the action. For a books resource the full surface looks like this:

GET /api/books list books (with filters and pagination) GET /api/books/:id fetch one book POST /api/books create a book PUT /api/books/:id replace a book PATCH /api/books/:id update some fields DELETE /api/books/:id remove a book

URLs contain plural nouns, never verbs. /api/getBooks or /api/books/delete are signs of a design going wrong.

The Model

js
// models/Book.js const mongoose = require('mongoose'); const bookSchema = new mongoose.Schema({ title: { type: String, required: true, trim: true }, author: { type: String, required: true, trim: true }, year: { type: Number, min: 0 }, genre: { type: String, index: true }, }, { timestamps: true }); module.exports = mongoose.model('Book', bookSchema);

List with Filtering and Pagination

Never return an unbounded collection. Support query parameters for filtering and paging from day one:

js
// routes/books.js const { Router } = require('express'); const Book = require('../models/Book'); const router = Router(); router.get('/', async (req, res) => { const page = Math.max(1, Number(req.query.page) || 1); const limit = Math.min(50, Number(req.query.limit) || 10); const filter = {}; if (req.query.genre) filter.genre = req.query.genre; const [items, total] = await Promise.all([ Book.find(filter).skip((page - 1) * limit).limit(limit), Book.countDocuments(filter), ]); res.json({ data: items, meta: { page, limit, total } }); });

Capping limit at 50 protects the server from a client asking for a million records. The meta object lets clients build pagination controls without guessing.

Create, Read, Update, Delete

js
router.post('/', async (req, res) => { const { title, author, year, genre } = req.body; const book = await Book.create({ title, author, year, genre }); res.status(201).location('/api/books/' + book._id).json({ data: book }); }); router.get('/:id', async (req, res) => { const book = await Book.findById(req.params.id); if (!book) return res.status(404).json({ error: 'Book not found' }); res.json({ data: book }); }); router.delete('/:id', async (req, res) => { const book = await Book.findByIdAndDelete(req.params.id); if (!book) return res.status(404).json({ error: 'Book not found' }); res.status(204).end(); });

Notice the details that separate a professional API from a toy: destructuring the body so clients cannot inject unexpected fields, a Location header on creation, and 204 No Content for successful deletes.

Consistent Response Shapes

Clients suffer when every endpoint invents its own format. Pick one envelope and use it everywhere. In this API, success responses are { data, meta? } and failures are { error }. Document that contract and honor it even in edge cases; consistency is a feature.

Versioning

APIs evolve. Mounting routers under a version prefix lets you introduce breaking changes without stranding old clients:

js
app.use('/api/v1/books', booksRouter);

When v2 arrives, both versions can run side by side while consumers migrate.

Key Takeaways

  • Model resources as plural nouns and let HTTP methods express actions.
  • Always paginate list endpoints and cap the page size.
  • Use 201 with a Location header for creates and 204 for deletes.
  • Whitelist body fields via destructuring; never pass req.body straight to the database.
  • Keep one consistent response envelope and version your API from the start.

Next lesson: Error Handling in Express, so failures produce clean responses instead of crashes.

Building a REST API with Express - Express.js | CodeYourCraft | CodeYourCraft