Introduction to Express.js

Building web applications with Node.js made simple

What is Express.js?

Express.js (or simply Express) is a minimal and flexible web application framework for Node.js. It provides a robust set of features for building single-page, multi-page, and hybrid web applications, as well as APIs. Express has become the de facto standard framework for Node.js web application development due to its simplicity, flexibility, and extensive ecosystem.

Definition

Express.js is a fast, unopinionated, minimalist web framework for Node.js that simplifies the process of building web applications and APIs by providing a thin layer of fundamental web application features without obscuring Node.js features.

flowchart TD A[Node.js HTTP Module] -->|Builds upon| B[Express.js] B -->|Simplifies| C[Routing] B -->|Provides| D[Middleware Architecture] B -->|Enables| E[Request/Response Handling] B -->|Supports| F[Template Engines] B -->|Facilitates| G[Error Handling]

Why Use Express.js?

The LEGO Building Blocks Analogy

Think of Express.js as a specialized set of LEGO building blocks for constructing web applications:

  • Node.js provides the foundation board (the runtime environment)
  • Express.js gives you pre-designed blocks that fit together perfectly (routing, middleware, etc.)
  • You can add exactly the blocks you need without unnecessary pieces (minimalist approach)
  • Blocks can be assembled in various ways (flexibility in application structure)
  • Additional specialized block sets are available when needed (middleware modules)
  • You can still use the standard blocks alongside your specialized ones (direct access to Node.js features)

Just like LEGO, Express lets you build something simple quickly, yet provides all the pieces needed to construct highly complex structures when required.

Express.js vs. Vanilla Node.js

To understand the value Express brings, let's compare a simple "Hello World" server in vanilla Node.js versus Express:

Vanilla Node.js

// Vanilla Node.js Server
const http = require('http');

const server = http.createServer((req, res) => {
  // Parse URL to extract path
  const url = req.url;
  
  // Route handling
  if (url === '/') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>Hello, World!</h1>');
  } else if (url === '/about') {
    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>About Page</h1>');
  } else if (url === '/api/users') {
    const users = [
      { id: 1, name: 'John' },
      { id: 2, name: 'Jane' }
    ];
    
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify(users));
  } else {
    res.writeHead(404, { 'Content-Type': 'text/html' });
    res.end('<h1>404 Not Found</h1>');
  }
});

const PORT = 3000;

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Express.js

// Express.js Server
const express = require('express');
const app = express();

// Route handling
app.get('/', (req, res) => {
  res.send('<h1>Hello, World!</h1>');
});

app.get('/about', (req, res) => {
  res.send('<h1>About Page</h1>');
});

app.get('/api/users', (req, res) => {
  const users = [
    { id: 1, name: 'John' },
    { id: 2, name: 'Jane' }
  ];
  
  res.json(users);
});

// 404 route (must be defined last)
app.use((req, res) => {
  res.status(404).send('<h1>404 Not Found</h1>');
});

const PORT = 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Key Differences

Feature Vanilla Node.js Express.js
Routing Manual URL parsing and conditional logic Method-based routing (app.get, app.post, etc.)
Content Type Manual header setting (res.writeHead) Automatic content type inference (res.send, res.json)
Status Codes Set in writeHead method Chainable methods (res.status(404).send())
Error Handling Manual try/catch blocks Built-in error handling middleware
Middleware Must be implemented manually Rich middleware ecosystem
Extensions Custom implementation required Wide range of plug-and-play modules
Code Complexity Increases rapidly with application size Scales well with modular approach

When to Use Vanilla Node.js vs. Express

Use Vanilla Node.js when:

  • Building extremely simple applications
  • Maximum performance is critical and you need to avoid any overhead
  • Learning the basics of Node.js before moving to frameworks
  • Developing specialized servers with unique requirements

Use Express.js when:

  • Building web applications or APIs of any significant complexity
  • You need a structured approach to routing and middleware
  • You want to leverage the ecosystem of middleware modules
  • Development speed and maintainability are priorities

Setting Up Express.js

Let's get started with Express by setting up a basic application.

Prerequisites

Step 1: Initialize a New Node.js Project

// Create a new directory for your project
mkdir express-demo
cd express-demo

// Initialize a new Node.js project
npm init -y

This creates a package.json file with default values. The -y flag accepts all defaults without prompting.

Step 2: Install Express

npm install express

This installs the Express framework and adds it to your project's dependencies in package.json.

Step 3: Create Your First Express Server

Create a file named app.js in your project directory:

// app.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON bodies
app.use(express.json());

// Middleware to serve static files
app.use(express.static('public'));

// Route for the homepage
app.get('/', (req, res) => {
  res.send('<h1>Welcome to Express!</h1>');
});

// Route with URL parameter
app.get('/users/:id', (req, res) => {
  res.send(`User ID: ${req.params.id}`);
});

// Route for API endpoint
app.get('/api/info', (req, res) => {
  res.json({
    name: 'Express Demo API',
    version: '1.0.0',
    status: 'active'
  });
});

// POST route example
app.post('/api/data', (req, res) => {
  console.log('Request Body:', req.body);
  res.status(201).json({
    message: 'Data received successfully',
    data: req.body
  });
});

// Start the server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Step 4: Create a Public Directory for Static Files

mkdir public

Create a file named index.html in the public directory:

// public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Express.js Demo</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    h1 {
      color: #333;
    }
    .container {
      border: 1px solid #ddd;
      padding: 20px;
      border-radius: 5px;
    }
    .btn {
      background-color: #4CAF50;
      color: white;
      padding: 10px 15px;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .response {
      margin-top: 20px;
      padding: 10px;
      background-color: #f5f5f5;
      border-radius: 4px;
      min-height: 100px;
    }
  </style>
</head>
<body>
  <h1>Express.js Demo</h1>
  
  <div class="container">
    <h2>Test API Endpoints</h2>
    
    <div>
      <button class="btn" id="getInfoBtn">Get API Info</button>
      <button class="btn" id="postDataBtn">Post Data</button>
    </div>
    
    <div class="response" id="responseContainer">
      Response will appear here...
    </div>
  </div>
  
  <script>
    // Get elements
    const getInfoBtn = document.getElementById('getInfoBtn');
    const postDataBtn = document.getElementById('postDataBtn');
    const responseContainer = document.getElementById('responseContainer');
    
    // Event listeners
    getInfoBtn.addEventListener('click', getApiInfo);
    postDataBtn.addEventListener('click', postData);
    
    // Get API info
    async function getApiInfo() {
      try {
        const response = await fetch('/api/info');
        const data = await response.json();
        responseContainer.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
      } catch (error) {
        responseContainer.innerHTML = 'Error: ' + error.message;
      }
    }
    
    // Post data
    async function postData() {
      try {
        const data = {
          name: 'Test User',
          email: 'test@example.com',
          message: 'Hello from the client!',
          timestamp: new Date().toISOString()
        };
        
        const response = await fetch('/api/data', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data)
        });
        
        const responseData = await response.json();
        responseContainer.innerHTML = '<pre>' + JSON.stringify(responseData, null, 2) + '</pre>';
      } catch (error) {
        responseContainer.innerHTML = 'Error: ' + error.message;
      }
    }
  </script>
</body>
</html>

Step 5: Run Your Express Server

node app.js

Open your browser and navigate to http://localhost:3000 to see your application in action.

flowchart LR Client[Web Browser] -->|HTTP Request| Server[Express Server] Server -->|HTTP Response| Client subgraph Server A[Express App] B[Middleware] C[Routes] D[Static Files] A --> B B --> C C --> D end

Express.js Core Concepts

The Application Object

The express() function creates an Express application object, which has methods for:

const express = require('express');
const app = express(); // Create an Express application

// Configuration
app.set('view engine', 'ejs');
app.set('views', './views');

// Start the server
app.listen(3000, () => {
  console.log('Server started on port 3000');
});

Routing

Routing refers to determining how an application responds to a client request to a particular endpoint, which is a URI (or path) and a specific HTTP request method (GET, POST, etc.).

// Basic route structure
app.METHOD(PATH, HANDLER);

// Examples
app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.post('/login', (req, res) => {
  // Handle login logic
  res.send('Login processed');
});

app.put('/users/:id', (req, res) => {
  res.send(`Update user with ID: ${req.params.id}`);
});

app.delete('/users/:id', (req, res) => {
  res.send(`Delete user with ID: ${req.params.id}`);
});

Route Parameters

Route parameters are named URL segments used to capture values at specific positions in the URL. The captured values are stored in the req.params object.

// Route with parameters
app.get('/users/:userId/books/:bookId', (req, res) => {
  res.send(`User ID: ${req.params.userId}, Book ID: ${req.params.bookId}`);
});

// Example URL: /users/34/books/8989
// req.params: { "userId": "34", "bookId": "8989" }

Middleware

Middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application's request-response cycle. Middleware can:

flowchart LR A[Client Request] --> B[Middleware 1] B --> C[Middleware 2] C --> D[Middleware 3] D --> E[Route Handler] E --> F[Response]
// Middleware function structure
function myMiddleware(req, res, next) {
  // Do something with req and res
  console.log('Middleware executed');
  
  // Call next() to pass control to the next middleware
  next();
}

// Apply middleware to all routes
app.use(myMiddleware);

// Apply middleware to a specific route
app.get('/protected', myMiddleware, (req, res) => {
  res.send('Protected route');
});

// Middleware for error handling
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).send('Something broke!');
});

Common Express Middleware

  • express.json(): Parses incoming requests with JSON payloads
  • express.urlencoded(): Parses incoming requests with URL-encoded payloads
  • express.static(): Serves static files
  • morgan: HTTP request logger
  • cors: Enables Cross-Origin Resource Sharing
  • helmet: Helps secure Express apps with various HTTP headers
  • compression: Compresses response bodies
// Example of using common middleware
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');
const helmet = require('helmet');
const compression = require('compression');

const app = express();

// Apply middleware
app.use(morgan('dev')); // Logging
app.use(cors()); // Enable CORS for all routes
app.use(helmet()); // Security headers
app.use(compression()); // Compress responses
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies

Request and Response Objects

Express.js extends Node.js's request and response objects with additional properties and methods that simplify common tasks.

Request Object (req)

Property/Method Description Example
req.params URL parameters /users/:idreq.params.id
req.query Query string parameters /search?q=expressreq.query.q
req.body Request body (requires middleware) req.body.username
req.headers HTTP headers req.headers['user-agent']
req.cookies Cookies (requires cookie-parser) req.cookies.sessionId
req.path Path part of URL /users/profilereq.path
req.method HTTP method 'GET', 'POST', etc.

Response Object (res)

Method Description Example
res.send() Sends the HTTP response res.send('Hello World!');
res.json() Sends a JSON response res.json({ name: 'John' });
res.status() Sets the HTTP status code res.status(404).send('Not Found');
res.sendFile() Sends a file res.sendFile('index.html');
res.render() Renders a view template res.render('index', { title: 'Home' });
res.redirect() Redirects to another URL res.redirect('/login');
res.cookie() Sets a cookie res.cookie('session', '12345');
res.clearCookie() Clears a cookie res.clearCookie('session');
// Example using request and response objects
app.get('/api/search', (req, res) => {
  const query = req.query.q;
  const limit = parseInt(req.query.limit) || 10;
  
  if (!query) {
    return res.status(400).json({
      error: 'Missing search query'
    });
  }
  
  // Log headers for debugging
  console.log('Request Headers:', req.headers);
  
  // Perform search (mock data)
  const results = mockDatabase.search(query, limit);
  
  // Send JSON response with search results
  res.json({
    query,
    limit,
    count: results.length,
    results
  });
});

Real-World Express.js Application Structure

In real-world applications, you'll want to organize your code in a modular way. Here's a common project structure for Express applications:

project-root/
  ├── node_modules/
  ├── public/
  │   ├── css/
  │   ├── js/
  │   └── images/
  ├── src/
  │   ├── config/
  │   │   └── database.js
  │   ├── controllers/
  │   │   ├── userController.js
  │   │   └── productController.js
  │   ├── middleware/
  │   │   ├── auth.js
  │   │   └── errorHandler.js
  │   ├── models/
  │   │   ├── User.js
  │   │   └── Product.js
  │   ├── routes/
  │   │   ├── userRoutes.js
  │   │   └── productRoutes.js
  │   └── utils/
  │       └── helpers.js
  ├── views/
  │   ├── layouts/
  │   │   └── main.ejs
  │   ├── partials/
  │   │   ├── header.ejs
  │   │   └── footer.ejs
  │   ├── index.ejs
  │   └── user.ejs
  ├── app.js
  ├── package.json
  └── README.md

Let's look at how each component would be organized:

Main Application File (app.js)

// app.js
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const errorHandler = require('./src/middleware/errorHandler');

// Import routes
const userRoutes = require('./src/routes/userRoutes');
const productRoutes = require('./src/routes/productRoutes');

// Create Express app
const app = express();

// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// Middleware
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

// Routes
app.use('/api/users', userRoutes);
app.use('/api/products', productRoutes);

// Home route
app.get('/', (req, res) => {
  res.render('index', { title: 'Express App' });
});

// Error handling middleware (should be last)
app.use(errorHandler);

// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = app;

Routes File

// src/routes/userRoutes.js
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const authMiddleware = require('../middleware/auth');

// GET all users
router.get('/', userController.getAllUsers);

// GET single user
router.get('/:id', userController.getUserById);

// POST new user
router.post('/', userController.createUser);

// PUT update user (protected route requiring authentication)
router.put('/:id', authMiddleware.verifyToken, userController.updateUser);

// DELETE user (protected route requiring authentication)
router.delete('/:id', authMiddleware.verifyToken, userController.deleteUser);

module.exports = router;

Controller File

// src/controllers/userController.js
const User = require('../models/User');

// Get all users
exports.getAllUsers = async (req, res, next) => {
  try {
    const users = await User.findAll();
    res.status(200).json(users);
  } catch (error) {
    next(error); // Pass to error handler middleware
  }
};

// Get user by ID
exports.getUserById = async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id);
    
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    res.status(200).json(user);
  } catch (error) {
    next(error);
  }
};

// Create new user
exports.createUser = async (req, res, next) => {
  try {
    const { name, email, password } = req.body;
    
    // Validate input
    if (!name || !email || !password) {
      return res.status(400).json({ message: 'Please provide name, email, and password' });
    }
    
    // Check if user already exists
    const existingUser = await User.findByEmail(email);
    if (existingUser) {
      return res.status(409).json({ message: 'Email already in use' });
    }
    
    // Create user
    const newUser = await User.create({ name, email, password });
    
    res.status(201).json({
      message: 'User created successfully',
      user: {
        id: newUser.id,
        name: newUser.name,
        email: newUser.email
      }
    });
  } catch (error) {
    next(error);
  }
};

// Update user
exports.updateUser = async (req, res, next) => {
  try {
    const { name, email } = req.body;
    const userId = req.params.id;
    
    // Check if user exists
    const user = await User.findById(userId);
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    // Update user
    const updatedUser = await User.update(userId, { name, email });
    
    res.status(200).json({
      message: 'User updated successfully',
      user: updatedUser
    });
  } catch (error) {
    next(error);
  }
};

// Delete user
exports.deleteUser = async (req, res, next) => {
  try {
    const userId = req.params.id;
    
    // Check if user exists
    const user = await User.findById(userId);
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    // Delete user
    await User.delete(userId);
    
    res.status(200).json({
      message: 'User deleted successfully'
    });
  } catch (error) {
    next(error);
  }
};

Middleware File

// src/middleware/auth.js
const jwt = require('jsonwebtoken');

exports.verifyToken = (req, res, next) => {
  // Get auth header
  const authHeader = req.headers.authorization;
  
  if (!authHeader) {
    return res.status(401).json({ message: 'Access denied. No token provided.' });
  }
  
  // Check if bearer token
  const parts = authHeader.split(' ');
  if (parts.length !== 2 || parts[0] !== 'Bearer') {
    return res.status(401).json({ message: 'Token format is invalid.' });
  }
  
  const token = parts[1];
  
  try {
    // Verify token
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    // Add user data to request
    req.user = decoded;
    
    // Proceed to the next middleware or route handler
    next();
  } catch (error) {
    return res.status(401).json({ message: 'Invalid token.' });
  }
};

Error Handler Middleware

// src/middleware/errorHandler.js
// Central error handling middleware
module.exports = (err, req, res, next) => {
  // Log error
  console.error(err.stack);
  
  // Set default error status and message
  const status = err.statusCode || 500;
  const message = err.message || 'Something went wrong on the server';
  
  // Send error response
  res.status(status).json({
    status: 'error',
    statusCode: status,
    message
  });
};

Express.js Best Practices

  1. Use environment variables for configuration (port, database credentials, secret keys)
  2. Implement proper error handling with middleware
  3. Structure your application in a modular way (routes, controllers, models)
  4. Use async/await for cleaner asynchronous code
  5. Validate user input to prevent security issues
  6. Set appropriate security headers with middleware like helmet
  7. Use logging for debugging and monitoring
  8. Implement rate limiting to prevent abuse
  9. Use compression to improve performance
  10. Follow RESTful principles for API design

Practical Exercise

Exercise: Build a RESTful API for a Task Manager

Let's create a simple RESTful API for a task manager application using Express.js.

Step 1: Setup Project

mkdir task-manager-api
cd task-manager-api
npm init -y
npm install express uuid

Step 2: Create the Main Application File

Create a file named app.js:

// app.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const taskRoutes = require('./routes/tasks');

// Middleware to parse JSON
app.use(express.json());

// Simple logging middleware
app.use((req, res, next) => {
  console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
  next();
});

// Root route
app.get('/', (req, res) => {
  res.send('Task Manager API - Use /api/tasks to access the API');
});

// Task routes
app.use('/api/tasks', taskRoutes);

// Error handling middleware
app.use((req, res, next) => {
  res.status(404).json({ message: 'Route not found' });
});

app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Internal server error' });
});

// Start server
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = app;

Step 3: Create Routes Directory and Tasks Routes

mkdir routes

Create routes/tasks.js:

// routes/tasks.js
const express = require('express');
const router = express.Router();
const { 
  getAllTasks, 
  getTaskById, 
  createTask, 
  updateTask, 
  deleteTask 
} = require('../controllers/taskController');

// GET all tasks
router.get('/', getAllTasks);

// GET single task
router.get('/:id', getTaskById);

// POST new task
router.post('/', createTask);

// PUT update task
router.put('/:id', updateTask);

// DELETE task
router.delete('/:id', deleteTask);

module.exports = router;

Step 4: Create Controllers Directory and Task Controller

mkdir controllers

Create controllers/taskController.js:

// controllers/taskController.js
const { v4: uuidv4 } = require('uuid');

// In-memory database
let tasks = [
  {
    id: '1',
    title: 'Complete project proposal',
    description: 'Write up the project proposal for the client',
    status: 'pending',
    created_at: '2025-05-01T10:30:00.000Z'
  },
  {
    id: '2',
    title: 'Buy groceries',
    description: 'Milk, eggs, bread, and vegetables',
    status: 'completed',
    created_at: '2025-05-02T14:20:00.000Z'
  }
];

// Get all tasks
exports.getAllTasks = (req, res) => {
  // Support filtering by status
  const { status } = req.query;
  
  if (status) {
    const filteredTasks = tasks.filter(task => task.status === status);
    return res.json(filteredTasks);
  }
  
  res.json(tasks);
};

// Get task by ID
exports.getTaskById = (req, res) => {
  const task = tasks.find(task => task.id === req.params.id);
  
  if (!task) {
    return res.status(404).json({ message: 'Task not found' });
  }
  
  res.json(task);
};

// Create new task
exports.createTask = (req, res) => {
  const { title, description, status } = req.body;
  
  // Validate input
  if (!title) {
    return res.status(400).json({ message: 'Title is required' });
  }
  
  const newTask = {
    id: uuidv4(),
    title,
    description: description || '',
    status: status || 'pending',
    created_at: new Date().toISOString()
  };
  
  tasks.push(newTask);
  
  res.status(201).json(newTask);
};

// Update task
exports.updateTask = (req, res) => {
  const { title, description, status } = req.body;
  const taskId = req.params.id;
  
  // Find task index
  const taskIndex = tasks.findIndex(task => task.id === taskId);
  
  if (taskIndex === -1) {
    return res.status(404).json({ message: 'Task not found' });
  }
  
  // Update task
  const updatedTask = {
    ...tasks[taskIndex],
    title: title || tasks[taskIndex].title,
    description: description !== undefined ? description : tasks[taskIndex].description,
    status: status || tasks[taskIndex].status,
    updated_at: new Date().toISOString()
  };
  
  tasks[taskIndex] = updatedTask;
  
  res.json(updatedTask);
};

// Delete task
exports.deleteTask = (req, res) => {
  const taskId = req.params.id;
  
  // Find task index
  const taskIndex = tasks.findIndex(task => task.id === taskId);
  
  if (taskIndex === -1) {
    return res.status(404).json({ message: 'Task not found' });
  }
  
  // Remove task
  tasks.splice(taskIndex, 1);
  
  res.json({ message: 'Task deleted successfully' });
};

Step 5: Test Your API

  1. Start your server:
    node app.js
  2. Use a tool like Postman, Insomnia, or curl to test your API endpoints:
    • GET all tasks: GET http://localhost:3000/api/tasks
    • GET task by ID: GET http://localhost:3000/api/tasks/1
    • Create task: POST http://localhost:3000/api/tasks with JSON body:
      {
        "title": "Learn Express.js",
        "description": "Complete Express.js tutorial and exercises",
        "status": "in-progress"
      }
    • Update task: PUT http://localhost:3000/api/tasks/1 with JSON body:
      {
        "status": "completed"
      }
    • Delete task: DELETE http://localhost:3000/api/tasks/2

Bonus Challenges

  1. Add Validation Middleware: Create a middleware that validates task data before it reaches the controller
  2. Implement Pagination: Add support for paginating tasks with limit and page query parameters
  3. Add Search Functionality: Allow searching tasks by title or description
  4. Create a Frontend: Build a simple HTML/CSS/JS frontend that consumes your API
  5. Persist Data: Replace the in-memory database with file-based storage using the fs module

Summary

Further Reading