Beginner

Build a REST API with Node and Express

Build a RESTful API with Express — read endpoints, project architecture and full CRUD.

Node.js · Express · REST · API Architecture


Table of Contents

  1. Introduction to RESTful APIs
  2. Creating Data Read Endpoints
  3. Organization and Architecture of a REST API
  4. Implementing Create, Update, Delete Endpoints
  5. Reference Tables
  6. Architecture Diagrams

1. Introduction to RESTful APIs

What Is a REST API?

An API (Application Programming Interface) is a set of rules defining how software applications communicate with each other. A REST API is an API that follows REST (Representational State Transfer) design principles — a set of standards defining a uniform and predictable way for applications to exchange data.

Unlike a traditional server application where the server is responsible for rendering HTML, a REST API simply exchanges JSON data between client and server. The client (e.g., a React application) sends a request, the server responds with JSON data — it’s the frontend’s job to decide how to display it.

Core REST Principles

PrincipleDescription
Separation of client and serverFrontend and backend are independent. The server exposes an interface, the client uses it
StatelessnessEach request must contain everything the server needs to understand it. Nothing about the client is stored on the server between requests
Uniform interfaceResources are identified by standardized URIs. HTTP methods define the actions
Layered systemThe client doesn’t know if it’s communicating directly with the final server or through an intermediary (cache, load balancer, etc.)

Key advantage: Thanks to statelessness, a single backend can serve any number of clients — web browsers, mobile apps, other backends.

Defining API Endpoints

A client interacts with endpoints using HTTP methods. Each endpoint receives:

  • The resource name (e.g., /users, /patients)
  • The action to perform on that resource
  • The data needed to create or update the resource

In Express, each HTTP method corresponds to a framework method:

// Defining an endpoint in Express
app.get('/users/:id', (req, res) => {
  // Connect to DB, fetch user, return as JSON
  res.status(200).json(user);
});

CRUD and HTTP Methods

CRUD (Create, Read, Update, Delete) refers to the four basic data operations. Each maps to an HTTP method:

CRUD OperationHTTP MethodExample EndpointDescription
CreatePOSTPOST /patientsCreate a new resource
ReadGETGET /patientsRetrieve all resources
ReadGETGET /patients/:idRetrieve a specific resource
UpdatePUTPUT /patients/:idCompletely replace a resource
UpdatePATCHPATCH /patients/:idPartially update a resource
DeleteDELETEDELETE /patients/:idDelete a resource

2. Creating Data Read Endpoints

Planning API Routes

This course builds a REST API serving patient data — for a medical office, medical billing app, or virtual health service. Before coding, it’s essential to plan the routes:

MethodRouteDescription
GET/patientsRetrieve all patients
GET/patients/:idRetrieve a patient by ID
POST/patientsCreate a new patient
PUT/patients/:idUpdate an existing patient
DELETE/patients/:idDelete a patient

Best practice: Planning before building creates a solid blueprint for a well-structured API.

Project Structure

project/
├── app.js              # Express application entry point
├── helpers.js          # Utilities: DB read/write, ID generation
├── db.json             # JSON file simulating the database
├── package.json
├── routes/
│   └── patientRouter.js    # Modular routes for patients
└── services/
    └── patientService.js   # Business logic

package.json — Dependencies:

{
  "name": "nodeapis_example_project",
  "type": "module",
  "version": "1.0.0",
  "main": "app.js",
  "scripts": {
    "dev": "node --watch app.js"
  },
  "dependencies": {
    "express": "^4.19.2",
    "uuid": "^10.0.0"
  }
}

helpers.js — Utility functions for interacting with db.json:

import { promises as fs } from 'fs';
import { v4 as uuidv4 } from 'uuid';

const dataFile = 'db.json';

// Read data from the JSON file
async function readData() {
    try {
        const data = await fs.readFile(dataFile, { encoding: 'utf-8' });
        return JSON.parse(data);
    } catch (err) {
        console.error("Failed to read data:", err.message);
        throw new Error("Error reading data file.");
    }
}

// Write data to the JSON file
async function writeData(data) {
    try {
        await fs.writeFile(dataFile, JSON.stringify(data, null, 2));
        return "File written successfully!";
    } catch(error) {
        console.error("Failed to write data:", error.message);
        throw new Error("Error writing data file.");
    }
}

// Generate a unique ID using UUID v4
async function generateUniqueId() {
    return uuidv4();
}

export { readData, writeData, generateUniqueId };

Note: Normally, an API would interact with a database via an ORM (Sequelize, Mongoose, etc.). Here, a JSON file is used to simplify and focus on core concepts.

Retrieving All Resources

Initial implementation in app.js (before modularization):

import express from 'express';
import { readData } from './helpers.js';

const app = express();

// GET /patients — Retrieve all patients
app.get('/patients', async (req, res) => {
    try {
        const results = await readData();
        res.status(200).json(results);
    } catch(error) {
        console.log(error);
        res.status(500).json({ error: "Error fetching data." });
    }
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

Test: Run with npm run dev, then visit http://localhost:3000/patients in the browser.

Retrieving a Resource by ID

// GET /patients/:id — Retrieve a specific patient by ID
app.get('/patients/:id', async (req, res) => {
    try {
        const results = await readData();
        const id = req.params.id;   // Extract ID from URL parameters

        const patient = results.find(patient => patient.id === id);
        if (!patient) {
            console.error(`Patient not found for ID: ${id}`);
            return res.status(404).json({ error: "Patient not found." });
        }
        res.status(200).json(patient);
    } catch(error) {
        console.log(error);
        res.status(500).json({ error: "Error fetching data." });
    }
});

URL parameter: req.params.id extracts the dynamic :id value from the URL. For example, for GET /patients/abc-123, req.params.id equals "abc-123".

HTTP Status Codes

Every API response should include an appropriate status code:

RangeCategoryCommon Examples
2xxSuccess200 OK, 201 Created, 204 No Content
3xxRedirection301 Moved Permanently, 302 Found
4xxClient error400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found
5xxServer error500 Internal Server Error, 503 Service Unavailable
CodeMeaningUse Case
200OKSuccessful GET or PUT with returned content
201CreatedResource successfully created (POST)
204No ContentSuccess without response body (DELETE, PUT without return)
400Bad RequestInvalid syntax, missing or malformed data
401UnauthorizedAuthentication required or failed
403ForbiddenAuthenticated but not authorized
404Not FoundResource not found
500Internal Server ErrorUnexpected server-side error

3. Organization and Architecture of a REST API

Service Layer — Business Logic Layer

As the application grows, app.js becomes difficult to maintain. The Service Layer pattern separates:

  • Business logic → in services
  • HTTP request handling (route handling) → in routes/controllers

Business logic includes: database interactions, calculations, data validation, business rule management.

services/patientService.js — Isolated business logic:

import { readData } from '../helpers.js';

// Retrieve all patients
async function getAllPatients() {
    return await readData();
}

// Retrieve a patient by ID
async function getPatientById(id) {
    const patients = await readData();
    const patient = patients.find(patient => patient.id === id);
    if (!patient) throw new Error('notfound');  // Error propagated to global handler
    return patient;
}

export { getAllPatients, getPatientById };

Global Error Handler

Instead of repeating the same error logic in each route, Express allows implementing a global error handling middleware. It’s a special middleware recognized by its 4 parameters: (error, req, res, next).

// app.js — place AFTER all routes
app.use((error, req, res, next) => {
    console.error(error);
    if (error.message === 'notfound') {
        res.status(404).json({ error: "No patient found with the provided ID." });
    } else {
        res.status(500).json({
            error: "An internal server error occurred."
        });
    }
});

Global Error Handler advantages:

  • Centralizes error handling in one place
  • Reduces code duplication (DRY principle)
  • Ensures consistent error responses
  • Easier maintenance

In routes, errors are forwarded via next(error):

router.get('/:id', async (req, res, next) => {
    try {
        const patient = await getPatientById(req.params.id);
        res.status(200).json(patient);
    } catch(error) {
        next(error);  // Pass error to the global error handler
    }
});

Modularizing Routes with Express.Router

express.Router is an Express tool for organizing routes into reusable and maintainable modules. Each resource gets its own route file.

routes/patientRouter.js:

import express from 'express';
const router = express.Router();

import { getAllPatients, getPatientById } from '../services/patientService.js';

// GET /patients — Retrieve all patients
router.get('/', async (req, res, next) => {
    try {
        const patients = await getAllPatients();
        res.status(200).json(patients);
    } catch(error) {
        next(error);
    }
});

// GET /patients/:id — Retrieve a patient by ID
router.get('/:id', async (req, res, next) => {
    try {
        const patient = await getPatientById(req.params.id);
        res.status(200).json(patient);
    } catch(error) {
        next(error);
    }
});

export default router;

app.js — Simplified entry point:

import express from 'express';
import patientRouter from './routes/patientRouter.js';

const app = express();

app.use(express.json());                  // Middleware: parse JSON body
app.use('/patients', patientRouter);     // Mount the router

// Global Error Handler (always last)
app.use((error, req, res, next) => {
    console.error(error);
    if (error.message === 'notfound') {
        res.status(404).json({ error: "No patient found with the provided ID." });
    } else {
        res.status(500).json({
            error: "An internal server error occurred."
        });
    }
});

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});

For a large application, the suggested structure is:

src/
├── routes/
│   ├── patientRouter.js
│   ├── prescriptionRouter.js
│   └── doctorRouter.js
├── services/
│   ├── patientService.js
│   ├── prescriptionService.js
│   └── doctorService.js
app.js
helpers.js
db.json
package.json

The src/ folder is common to separate source code from configuration files (package.json, .env, etc.).


4. Implementing Create, Update, Delete Endpoints

Creating a Resource — POST

Adding a new endpoint is a two-step process:

  1. Write a service function that interacts with the database
  2. Write a route that executes the function and returns a response

In patientService.js — Add createNewPatient:

import { readData, writeData, generateUniqueId } from '../helpers.js';

async function createNewPatient(newPatientData) {
    const patients = await readData();
    const newId = await generateUniqueId();   // Unique UUID v4

    const newPatient = {
        id: newId,
        ...newPatientData    // Spread operator to include all data
    };

    patients.push(newPatient);
    await writeData(patients);   // Persist to db.json
    return newPatient;
}

export { getAllPatients, getPatientById, createNewPatient };

In patientRouter.js — POST route:

import { getAllPatients, getPatientById, createNewPatient } from '../services/patientService.js';

// POST /patients — Create a new patient
router.post('/', async (req, res, next) => {
    try {
        const newPatient = await createNewPatient(req.body);  // Data from body
        res.status(201).json(newPatient);   // 201 Created
    } catch(error) {
        next(error);
    }
});

Important: express.json() must be enabled in app.js to parse the JSON body of POST/PUT requests: app.use(express.json());

Updating a Resource — PUT / PATCH

MethodUsage
PUTCompletely replaces a resource (all fields required)
PATCHPartially updates a resource (only sent fields)

In patientService.jsupdatePatient function:

async function updatePatient(id, newPatientData) {
    const patients = await readData();
    const patientIndex = patients.findIndex(p => p.id === id);

    if (patientIndex === -1) throw new Error('notfound');

    // Replace patient with new data (preserve ID)
    patients[patientIndex] = {
        id: patients[patientIndex].id,
        ...newPatientData
    };

    const updatedPatient = patients[patientIndex];
    await writeData(patients);
    return updatedPatient;
}

In patientRouter.js — PUT route:

// PUT /patients/:id — Update a patient
router.put('/:id', async (req, res, next) => {
    try {
        const updatedPatient = await updatePatient(req.params.id, req.body);
        res.status(200).json(updatedPatient);
    } catch(error) {
        next(error);
    }
});

Deleting a Resource — DELETE

In patientService.jsdeletePatient function:

async function deletePatient(id) {
    const patients = await readData();
    const originalLength = patients.length;

    // Filter out the patient to delete
    const updatedPatients = patients.filter(p => p.id !== id);

    // Verify deletion occurred
    if (updatedPatients.length === originalLength) {
        throw new Error('notfound');  // No patient deleted → invalid ID
    }

    await writeData(updatedPatients);
}

export { getAllPatients, getPatientById, createNewPatient, updatePatient, deletePatient };

In patientRouter.js — DELETE route:

import { getAllPatients, getPatientById, createNewPatient, updatePatient, deletePatient } from '../services/patientService.js';

// DELETE /patients/:id — Delete a patient
router.delete('/:id', async (req, res, next) => {
    try {
        await deletePatient(req.params.id);
        res.status(204).end();  // 204 No Content + end() to terminate response
    } catch(error) { next(error); }
});

export default router;

Important: After res.status(204), call .end() to terminate the response — otherwise the request remains pending until timeout.


5. Reference Tables

HTTP Methods and Their Semantics

MethodIdempotentSafeMain Usage
GETYesYesRead a resource (no side effects)
POSTNoNoCreate a new resource
PUTYesNoCompletely replace a resource
PATCHNoNoPartially modify a resource
DELETEYesNoDelete a resource
HEADYesYesLike GET but without response body
OPTIONSYesYesRetrieve supported methods (CORS preflight)

Idempotent: Calling the same request multiple times produces the same result. Safe: The request doesn’t modify server state.

Common Express Middleware

MiddlewareUsageExample
express.json()Parse JSON request bodyapp.use(express.json())
express.urlencoded()Parse HTML form dataapp.use(express.urlencoded({ extended: true }))
express.static()Serve static filesapp.use(express.static('public'))
morganHTTP request loggingapp.use(morgan('dev'))
corsCross-Origin Resource Sharing managementapp.use(cors())
helmetSecure HTTP headersapp.use(helmet())
Error handlerGlobal error handler (4 params)app.use((err, req, res, next) => {...})

Design Patterns in Express

PatternDescriptionBenefit
Service LayerSeparates business logic from routesMore testable and reusable code
Express.RouterModular routes per resourceBetter organization, more maintainable
Global Error HandlerCentralizes error handlingDRY, consistent error responses
Async/AwaitAsynchronous operations managementMore readable than callbacks
Try/Catch + next()Error propagation to handlerSeparation of concerns

Express Object Variables (req / res)

Property/MethodDescriptionExample
req.paramsDynamic URL parametersreq.params.id/patients/:id
req.queryQuery string parametersreq.query.name/patients?name=John
req.bodyRequest body (JSON/form)req.body.patient_name
req.headersRequest headersreq.headers.authorization
res.json(data)Send a JSON responseres.json({ name: "John" })
res.status(code)Set the status coderes.status(201)
res.send(text)Send a text responseres.send("Hello!")
res.end()End the response without bodyres.status(204).end()
next(error)Pass to next middleware/errornext(new Error('notfound'))

6. Architecture Diagrams

REST Architecture — Overview

graph LR
    subgraph Clients
        C1[Browser / React]
        C2[Mobile App]
        C3[API Consumer]
    end

    subgraph "REST API (Node + Express)"
        MW[Middleware Layer\nexpress.json · cors · helmet]
        R[Routes Layer\npatientRouter.js]
        S[Service Layer\npatientService.js]
        H[Helpers\nhelpers.js]
    end

    subgraph "Data Store"
        DB[(db.json\nSimulated Database)]
    end

    C1 -->|HTTP Request + JSON| MW
    C2 -->|HTTP Request + JSON| MW
    C3 -->|HTTP Request + JSON| MW
    MW --> R
    R --> S
    S --> H
    H --> DB
    DB --> H
    H --> S
    S --> R
    R -->|JSON Response + Status Code| C1
    R -->|JSON Response + Status Code| C2
    R -->|JSON Response + Status Code| C3

Express Middleware Flow

flowchart TD
    REQ[Incoming HTTP Request] --> MW1
    MW1[express.json\nParse body] --> MW2
    MW2[Other middleware\ncors · morgan · helmet] --> ROUTER
    ROUTER{Route matching\napp.use '/patients'} --> FOUND
    ROUTER -->|No route| NOT_FOUND[404 Not Found]
    FOUND[patientRouter.js] --> TRY
    TRY{try/catch}
    TRY -->|Success| SERVICE[Service Function\nbusiness logic]
    TRY -->|Error| NEXT[next error]
    SERVICE --> DB[DB Operation\nread · write]
    DB --> RESP[res.status\n.json\n.end]
    NEXT --> ERR_HANDLER[Global Error Handler\nerror, req, res, next]
    ERR_HANDLER -->|error.message === notfound| R404[res.status 404 .json]
    ERR_HANDLER -->|Other error| R500[res.status 500 .json]
    RESP --> CLIENT[JSON Response to client]
    R404 --> CLIENT
    R500 --> CLIENT

Search Terms

rest · api · node · express · node.js · apis · microservices · backend · full-stack · web · resource · architecture · endpoints · http · delete · methods · middleware · retrieving · routes

Interested in this course?

Contact us to book it or get a custom training plan for your team.