Unit 4.1 · Tutorial 2

Advanced Express & Application Structure

Chapter 10 · Backend Development
~3 hours Advanced Error Handling · Async · Architecture · Validation

Overview

Building production‑ready Express applications requires more than just routes. This tutorial covers advanced topics: error handling, asynchronous patterns, modular architecture, environment configuration, and request validation. You'll learn to structure applications that are maintainable and scalable.

Why this matters: Real‑world applications need to handle errors gracefully, manage complexity, and be secure. These advanced techniques are essential for professional backend development.

1. Error Handling

Error handling middleware has four parameters: `(err, req, res, next)`.

// Global error handler (place after all routes) app.use((err, req, res, next) => { console.error(err.stack); const statusCode = err.statusCode || 500; const message = err.message || 'Internal server error'; res.status(statusCode).json({ error: message, timestamp: new Date().toISOString(), path: req.url, }); }); // Throwing errors in route handlers app.get('/users/:id', (req, res, next) => { const user = findUser(req.params.id); if (!user) { const err = new Error('User not found'); err.statusCode = 404; return next(err); // Pass to error handler } res.json(user); });

Custom Error Classes

class AppError extends Error { constructor(message, statusCode) { super(message); this.statusCode = statusCode; this.isOperational = true; } } // Usage throw new AppError('Invalid email format', 400);

2. Asynchronous Patterns

Use `async/await` with error handling for cleaner asynchronous code.

// Async route handler with try/catch app.get('/users', async (req, res, next) => { try { const users = await User.find(); res.json(users); } catch (error) { next(error); } }); // Using express-async-errors (eliminates try/catch) require('express-async-errors'); app.get('/users', async (req, res) => { // Any error is automatically passed to the error handler const users = await User.find(); res.json(users); });

Async wrapper utility

const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; app.get('/users', asyncHandler(async (req, res) => { const users = await User.find(); res.json(users); }));

3. Modular Architecture

Organise your Express application into modules for maintainability.

// Recommended project structure src/ ├── app.js # Express app setup ├── server.js # Server entry point ├── routes/ # Route definitions │ ├── users.routes.js │ └── products.routes.js ├── controllers/ # Business logic handlers │ ├── users.controller.js │ └── products.controller.js ├── services/ # Business logic / database interactions │ ├── users.service.js │ └── products.service.js ├── models/ # Data models (Prisma, Mongoose) ├── middlewares/ # Custom middleware ├── utils/ # Utility functions └── config/ # Configuration

Router-level modularisation

// routes/users.routes.js const router = require('express').Router(); const { getUsers, createUser } = require('../controllers/users.controller'); router.get('/', getUsers); router.post('/', createUser); module.exports = router; // app.js const userRoutes = require('./routes/users.routes'); app.use('/api/users', userRoutes);

4. Environment Variables & Config

Use `dotenv` to manage environment variables for different deployment stages.

npm install dotenv
// .env PORT=3000 NODE_ENV=development DB_HOST=localhost DB_USER=postgres DB_PASSWORD=secret JWT_SECRET=my-secret-key
// config/index.js require('dotenv').config(); module.exports = { port: process.env.PORT || 3000, env: process.env.NODE_ENV || 'development', database: { host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, }, jwt: { secret: process.env.JWT_SECRET, expiresIn: process.env.JWT_EXPIRES_IN || '7d', }, };

5. Request Validation

Use `Joi` or `class-validator` to validate incoming requests.

npm install joi
const Joi = require('joi'); const userSchema = Joi.object({ name: Joi.string().min(2).max(100).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(0).max(150).optional(), }); // Validation middleware const validateUser = (req, res, next) => { const { error } = userSchema.validate(req.body); if (error) { return res.status(400).json({ error: 'Validation failed', details: error.details.map(d => d.message), }); } next(); }; app.post('/users', validateUser, (req, res) => { // req.body is valid res.json(req.body); });

Quiz

Question 1

What is the signature of an Express error-handling middleware?

  • (req, res, next)
  • (err, req, res, next)
  • (err, res, req, next)
  • (req, res, err, next)
Show answer
B. (err, req, res, next).

Question 2

Which package helps manage environment variables?

  • dotenv
  • env
  • config
  • environment
Show answer
A. dotenv.

Question 3

What is the purpose of the `express-async-errors` package?

  • To handle synchronous errors
  • To automatically catch errors from async route handlers
  • To improve performance
  • To add more middleware
Show answer
B. To automatically catch errors from async route handlers.

Exercises

Exercise 1

Add a global error handler to your Product API from the previous tutorial. It should log the error and return a JSON response with `error`, `timestamp`, and `path` fields.

Sample answer
app.use((err, req, res, next) => { console.error(err.stack); const status = err.statusCode || 500; res.status(status).json({ error: err.message || 'Internal server error', timestamp: new Date().toISOString(), path: req.url, }); });

Exercise 2

Refactor the Product API to use a modular structure: separate routes, controllers, and services. Create a `ProductService` that handles the in‑memory data operations.

Sample answer
  • services/product.service.js: `getAll`, `getById`, `create`, `update`, `delete` functions.
  • controllers/product.controller.js: Handlers that call the service.
  • routes/product.routes.js: Define routes and use controllers.
  • app.js: `app.use('/api/products', productRoutes)`.

Homework

Homework 1

Extend the User Management API with validation using Joi. Validate name (required, min 2 chars), email (required, valid format), and age (optional, number > 0). Add proper error handling.

Sample outline
  • Joi schema: `Joi.object({ name: Joi.string().min(2).required(), email: Joi.string().email().required(), age: Joi.number().integer().min(1).optional() })`.
  • Middleware: Validate request body before passing to controller.
  • Error: 400 with detailed validation errors.

Mini‑Project

Modular Task Manager API

Build a Task Manager API with a modular structure:

  • Routes, Controllers, Services, and Middleware directories
  • Tasks have: id, title, description, completed, userId
  • Joi validation for creating and updating tasks
  • Error handling with custom error classes
  • Environment variables for configuration (port, etc.)
  • Use in‑memory storage (an array)
Sample outline
  • Structure: `src/` with `routes/`, `controllers/`, `services/`, `middlewares/`, `config/`.
  • Service: `tasks.service.js` with CRUD operations.
  • Controller: `tasks.controller.js` that calls the service.
  • Middleware: `validateTask.js` using Joi.
  • Error Handler: Global error handler.

Tutorial Summary

You learned advanced Express patterns: global error handling, asynchronous patterns, modular project structure, environment configuration, and request validation. These techniques are essential for building production‑ready, maintainable backend applications.

Key takeaway: Structure your code for scalability and maintainability. Handle errors gracefully and validate inputs early.