ExpressJS - RESTful APIs
Building Modern RESTful APIs with Express.js
Section titled “Building Modern RESTful APIs with Express.js”A RESTful API (Representational State Transfer) is an architectural style for designing networked applications. It uses a stateless, client-server communication model, and leverages standard HTTP methods. For Express applications, this means creating well-structured endpoints that are intuitive, predictable, and easy for client applications (like mobile apps or single-page web apps) to consume.
A well-designed RESTful API uses HTTP verbs and resource-based URLs logically. Here’s a standard convention for a movies resource:
| Method | URI | Action | Description |
|---|---|---|---|
GET | /api/movies | Read | Retrieves a list of all movies. |
GET | /api/movies/:id | Read | Retrieves a single movie by its unique ID. |
POST | /api/movies | Create | Creates a new movie using the data in the request body. |
PUT | /api/movies/:id | Update | Replaces an entire existing movie with new data. |
PATCH | /api/movies/:id | Update | Partially updates an existing movie with new data. |
DELETE | /api/movies/:id | Delete | Deletes a movie by its unique ID. |
Let’s build this API following modern best practices. We’ll use an in-memory array for simplicity, but the structure is designed to easily swap in a real database.
Project Setup
Section titled “Project Setup”We will structure our code to separate concerns: routes, controllers, and data logic.
1. Main Application File (app.js)
Section titled “1. Main Application File (app.js)”This file sets up the Express app, applies global middleware, and mounts our API routes. Notice that body-parser is no longer needed; we use the built-in express.json().
import express from 'express';import movieRoutes from './movie.routes.js';
const app = express();
// Global middleware to parse JSON request bodiesapp.use(express.json());
// Mount the movie router on the /api/movies pathapp.use('/api/movies', movieRoutes);
// A simple 404 handlerapp.use((req, res, next) => { res.status(404).json({ message: 'Resource not found' });});
export default app; // Export for testing and server.js2. In-Memory Data and Router (movie.routes.js)
Section titled “2. In-Memory Data and Router (movie.routes.js)”This file defines the routes and connects them to controller functions. It also holds our temporary in-memory data store.
import { Router } from 'express';import { getAllMovies, getMovieById, createMovie, updateMovie, deleteMovie,} from './movie.controller.js';
const router = Router();
// In-memory data store (replace with a database in a real app)export let movies = [ { id: 1, title: 'Fight Club', year: 1999, rating: 8.8 }, { id: 2, title: 'Inception', year: 2010, rating: 8.7 }, { id: 3, title: 'The Dark Knight', year: 2008, rating: 9.0 },];
router.route('/') .get(getAllMovies) .post(createMovie);
router.route('/:id') .get(getMovieById) .put(updateMovie) .delete(deleteMovie);
export default router;Controller Logic (movie.controller.js)
Section titled “Controller Logic (movie.controller.js)”Controllers handle the request and response cycle. They contain the main logic for each route, keeping the route definitions clean. Note: In a real app, the logic for finding/updating data would be in a separate ‘service’ or ‘model’ layer.
import { movies } from './movie.routes.js'; // Note: This tight coupling is for example simplicity
let nextId = 4; // Simple ID incrementer
// GET /api/moviesexport const getAllMovies = (req, res) => { res.status(200).json(movies);};
// GET /api/movies/:idexport const getMovieById = (req, res) => { const movie = movies.find(m => m.id === parseInt(req.params.id)); if (!movie) { return res.status(404).json({ message: 'Movie not found' }); } res.status(200).json(movie);};
// POST /api/moviesexport const createMovie = (req, res) => { const { title, year, rating } = req.body; // Basic validation if (!title || !year || !rating) { return res.status(400).json({ message: 'Bad Request: Missing required fields' }); } const newMovie = { id: nextId++, title, year, rating }; movies.push(newMovie); res.status(201).json({ message: 'New movie created.', location: `/api/movies/${newMovie.id}` });};
// PUT /api/movies/:idexport const updateMovie = (req, res) => { const movieIndex = movies.findIndex(m => m.id === parseInt(req.params.id)); if (movieIndex === -1) { return res.status(404).json({ message: 'Movie not found' }); } const { title, year, rating } = req.body; if (!title || !year || !rating) { return res.status(400).json({ message: 'Bad Request: Missing required fields' }); } const updatedMovie = { id: parseInt(req.params.id), title, year, rating }; movies[movieIndex] = updatedMovie; res.status(200).json({ message: `Movie with id ${req.params.id} updated.`, movie: updatedMovie });};
// DELETE /api/movies/:idexport const deleteMovie = (req, res) => { const movieIndex = movies.findIndex(m => m.id === parseInt(req.params.id)); if (movieIndex === -1) { return res.status(404).json({ message: 'Movie not found' }); } movies.splice(movieIndex, 1); res.status(204).send(); // 204 No Content is a common response for successful deletion};Testing the API with curl
Section titled “Testing the API with curl”You can use a tool like curl or Postman to interact with your running API.
Create a new movie (POST)
Section titled “Create a new movie (POST)”curl -X POST http://localhost:3000/api/movies \-H "Content-Type: application/json" \-d '{"title": "The Matrix", "year": 1999, "rating": 8.7}'Response:
{ "message": "New movie created.", "location": "/api/movies/4"}Get a specific movie (GET)
Section titled “Get a specific movie (GET)”curl http://localhost:3000/api/movies/3Response:
{ "id": 3, "title": "The Dark Knight", "year": 2008, "rating": 9.0}Next Steps: Improving the API
Section titled “Next Steps: Improving the API”- Input Validation: The current validation is basic. Use a library like
joiorexpress-validatorto create robust, reusable validation schemas that are applied as middleware before the controller logic runs. - Database Integration: Replace the in-memory
moviesarray with a persistent database connection (e.g., MongoDB with Mongoose, or PostgreSQL with an ORM like Sequelize or Prisma). - Centralized Error Handling: Implement a global error-handling middleware to catch errors from controllers (especially from
asyncdatabase operations) and return consistent, formatted error responses.