Advanced Express.js: Dynamic Routing, Project Structure & API Best Practices

Anjali SainiAnjali Saini
5 min read
  • Advanced Express.js and Project Structuring

    1. Dynamic Routing in Express

    What is Dynamic Routing?

    Dynamic routing allows you to handle specific requests by embedding variable data in the route path. For example, instead of creating individual routes for each movie ID, dynamic routing allows /movies/:id to handle all movie IDs.

    Why Use Dynamic Routing?

    • Reduces redundancy by using a single route to handle multiple requests.

    • Enables URL parameters to pass unique data, like item IDs, directly in the URL path.

Route Parameters (/:id)

Route parameters are placeholders in the URL path, prefixed by :. For instance, /movies/:id can capture any value passed after /movies/ as req.params.id.

    app.get('/movies/:id', (req, res) => {
      const movieId = req.params.id;
      // Use movieId to find the specific movie in the database
    });

Query Strings (?key=value)

Query strings allow more complex requests by attaching key-value pairs to a URL. In /movies?title=Inception&year=2010, title and year are query parameters accessible via req.query.

    app.get('/movies', (req, res) => {
      const { title, year } = req.query;
      // Filter movies based on title or year
    });

2. Organizing Code with express.Router() and Folder Structure

express.Router()

express.Router() helps modularize route handling, allowing you to define routes in separate files for better organization.

Example of using express.Router():

  1. Create a Router in a Separate File (routes/movies.js):

     import express from 'express';
     const router = express.Router();
    
     // Define movie routes
     router.get('/', getAllMovies);
     router.get('/:id', getSingleMovie);
     router.post('/', createMovie);
    
     export default router;
    
  2. Use Router in Main App File (index.js):

     import express from 'express';
     import movieRoutes from './routes/movies.js';
    
     const app = express();
     app.use('/movies', movieRoutes); // Mount router on /movies path
    

Suggested Folder Structure

A well-organized folder structure makes maintenance and scalability easier. Here’s a typical structure:

    movie-api/
    ├── controllers/          # Logic for handling routes
    │   └── movieController.js
    ├── routes/               # Route definitions
    │   └── movies.js
    ├── models/               # Data models or schemas
    ├── middleware/           # Custom middleware
    ├── db.json               # Data file (temporary storage for movies)
    ├── index.js              # Main entry point
    └── package.json

3. HTTP Status Codes and Best Practices

HTTP status codes provide information on the response outcome:

  • 200 OK: Successful GET request.

  • 201 Created: Successful POST request.

  • 400 Bad Request: Invalid client request, often due to incorrect data.

  • 404 Not Found: Resource not found, like a movie ID that doesn’t exist.

  • 500 Internal Server Error: Server-side error.

Best Practices:

  • Use appropriate status codes to communicate results effectively.

  • Return helpful error messages to guide clients when requests fail.

Example:

    app.get('/movies/:id', (req, res) => {
      const movie = findMovieById(req.params.id);
      if (!movie) {
        return res.status(404).json({ error: 'Movie not found' });
      }
      res.status(200).json(movie);
    });

4. Movie Project Enhancement

Step 1: Organize Project Files and Folders

Following our suggested folder structure, split your route logic and controllers. Here’s how:

  1. Create Controller Functions in controllers/movieController.js:

     import fs from 'fs';
    
     export const getAllMovies = (req, res) => {
       const movies = JSON.parse(fs.readFileSync('db.json'));
       const { title, year } = req.query;
    
       let filteredMovies = movies;
       if (title) filteredMovies = filteredMovies.filter(m => m.title.toLowerCase().includes(title.toLowerCase()));
       if (year) filteredMovies = filteredMovies.filter(m => m.year === parseInt(year));
    
       res.status(200).json(filteredMovies);
     };
    
     export const getSingleMovie = (req, res) => {
       const movies = JSON.parse(fs.readFileSync('db.json'));
       const movie = movies.find(m => m.id === parseInt(req.params.id));
       if (!movie) return res.status(404).json({ error: 'Movie not found' });
    
       res.status(200).json(movie);
     };
    
     export const createMovie = (req, res) => {
       const movies = JSON.parse(fs.readFileSync('db.json'));
       const { title, year } = req.body;
    
       if (!title || !year) return res.status(400).json({ error: 'Title and year are required' });
    
       const newMovie = { id: Date.now(), title, year };
       movies.push(newMovie);
       fs.writeFileSync('db.json', JSON.stringify(movies));
    
       res.status(201).json(newMovie);
     };
    
  2. Define Routes in routes/movies.js:

     import express from 'express';
     import { getAllMovies, getSingleMovie, createMovie } from '../controllers/movieController.js';
    
     const router = express.Router();
    
     router.get('/', getAllMovies);
     router.get('/:id', getSingleMovie);
     router.post('/', createMovie);
    
     export default router;
    
  3. Use Routes in Main App File index.js:

     import express from 'express';
     import movieRoutes from './routes/movies.js';
    
     const app = express();
     const PORT = 3000;
    
     app.use(express.json());
     app.use('/movies', movieRoutes);
    
     app.listen(PORT, () => {
       console.log(`Server running on http://localhost:${PORT}`);
     });
    

Step 2: Advanced Search and Filtering Using Query Parameters

Extend the getAllMovies function to filter movies by title or year using query parameters.

Example:

  • URL: /movies?title=Inception

  • URL: /movies?year=2010

This allows clients to search for movies by partial title or year.

Step 3: Data Validation in POST Requests

To ensure the data provided in requests is valid, implement basic data validation.

    if (!title || !year || typeof title !== 'string' || typeof year !== 'number') {
      return res.status(400).json({ error: 'Valid title and year are required' });
    }

Step 4: Improved Error Handling and Edge Case Management

Improved Error Handling:

  • Handle errors gracefully using try/catch blocks, especially around file operations and JSON parsing.

  • Return specific error messages and status codes for different scenarios.

Edge Cases:

  • Check for existing movie IDs before creating new movies.

  • Ensure routes handle missing or incorrect IDs gracefully.

    app.get('/movies/:id', (req, res) => {
      try {
        const movies = JSON.parse(fs.readFileSync('db.json'));
        const movie = movies.find(m => m.id === parseInt(req.params.id));

        if (!movie) {
          return res.status(404).json({ error: 'Movie not found' });
        }
        res.status(200).json(movie);
      } catch (error) {
        res.status(500).json({ error: 'Internal server error' });
      }
    });

Step 5: Best Practices for Production-Ready Code

  1. Consistent Error Messages: Ensure consistent and user-friendly error responses.

  2. Validations: Add more validations to ensure data integrity.

  3. Descriptive Status Codes: Always use appropriate HTTP status codes.

  4. Environment Variables: Store sensitive information like API keys or database paths in environment variables for security.


5. Try it Out

  1. Add More Filters: Extend the search functionality to filter by additional fields.

  2. Refactor Code: Move repeated code, like reading and writing to db.json, into a utility function.

  3. Improved Validation: Use a validation library (like Joi or express-validator) to validate data on incoming requests.


6. Summary

  • Dynamic Routing and query parameters enable flexible routes and advanced filtering.

  • express.Router() modularizes routes for easier project management.

  • Use HTTP status codes and error handling for reliable client-server communication.

  • Project enhancements, including organized structure and data validation, create a more scalable and user-friendly API.


0
Subscribe to my newsletter

Read articles from Anjali Saini directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Anjali Saini
Anjali Saini

I am an Enthusiastic and self-motivated web-Developer . Currently i am learning to build end-to-end web-apps.