Node.js · Express · REST · API Architecture
Table of Contents
- Introduction to RESTful APIs
- Creating Data Read Endpoints
- Organization and Architecture of a REST API
- Implementing Create, Update, Delete Endpoints
- Reference Tables
- 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
| Principle | Description |
|---|---|
| Separation of client and server | Frontend and backend are independent. The server exposes an interface, the client uses it |
| Statelessness | Each request must contain everything the server needs to understand it. Nothing about the client is stored on the server between requests |
| Uniform interface | Resources are identified by standardized URIs. HTTP methods define the actions |
| Layered system | The 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 Operation | HTTP Method | Example Endpoint | Description |
|---|---|---|---|
| Create | POST | POST /patients | Create a new resource |
| Read | GET | GET /patients | Retrieve all resources |
| Read | GET | GET /patients/:id | Retrieve a specific resource |
| Update | PUT | PUT /patients/:id | Completely replace a resource |
| Update | PATCH | PATCH /patients/:id | Partially update a resource |
| Delete | DELETE | DELETE /patients/:id | Delete 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:
| Method | Route | Description |
|---|---|---|
GET | /patients | Retrieve all patients |
GET | /patients/:id | Retrieve a patient by ID |
POST | /patients | Create a new patient |
PUT | /patients/:id | Update an existing patient |
DELETE | /patients/:id | Delete 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.idextracts the dynamic:idvalue from the URL. For example, forGET /patients/abc-123,req.params.idequals"abc-123".
HTTP Status Codes
Every API response should include an appropriate status code:
| Range | Category | Common Examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirection | 301 Moved Permanently, 302 Found |
| 4xx | Client error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found |
| 5xx | Server error | 500 Internal Server Error, 503 Service Unavailable |
| Code | Meaning | Use Case |
|---|---|---|
200 | OK | Successful GET or PUT with returned content |
201 | Created | Resource successfully created (POST) |
204 | No Content | Success without response body (DELETE, PUT without return) |
400 | Bad Request | Invalid syntax, missing or malformed data |
401 | Unauthorized | Authentication required or failed |
403 | Forbidden | Authenticated but not authorized |
404 | Not Found | Resource not found |
500 | Internal Server Error | Unexpected 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');
});
Recommended File Structure
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:
- Write a service function that interacts with the database
- 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 inapp.jsto parse the JSON body of POST/PUT requests:app.use(express.json());
Updating a Resource — PUT / PATCH
| Method | Usage |
|---|---|
PUT | Completely replaces a resource (all fields required) |
PATCH | Partially updates a resource (only sent fields) |
In patientService.js — updatePatient 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.js — deletePatient 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
| Method | Idempotent | Safe | Main Usage |
|---|---|---|---|
GET | Yes | Yes | Read a resource (no side effects) |
POST | No | No | Create a new resource |
PUT | Yes | No | Completely replace a resource |
PATCH | No | No | Partially modify a resource |
DELETE | Yes | No | Delete a resource |
HEAD | Yes | Yes | Like GET but without response body |
OPTIONS | Yes | Yes | Retrieve 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
| Middleware | Usage | Example |
|---|---|---|
express.json() | Parse JSON request body | app.use(express.json()) |
express.urlencoded() | Parse HTML form data | app.use(express.urlencoded({ extended: true })) |
express.static() | Serve static files | app.use(express.static('public')) |
morgan | HTTP request logging | app.use(morgan('dev')) |
cors | Cross-Origin Resource Sharing management | app.use(cors()) |
helmet | Secure HTTP headers | app.use(helmet()) |
| Error handler | Global error handler (4 params) | app.use((err, req, res, next) => {...}) |
Design Patterns in Express
| Pattern | Description | Benefit |
|---|---|---|
| Service Layer | Separates business logic from routes | More testable and reusable code |
| Express.Router | Modular routes per resource | Better organization, more maintainable |
| Global Error Handler | Centralizes error handling | DRY, consistent error responses |
| Async/Await | Asynchronous operations management | More readable than callbacks |
| Try/Catch + next() | Error propagation to handler | Separation of concerns |
Express Object Variables (req / res)
| Property/Method | Description | Example |
|---|---|---|
req.params | Dynamic URL parameters | req.params.id → /patients/:id |
req.query | Query string parameters | req.query.name → /patients?name=John |
req.body | Request body (JSON/form) | req.body.patient_name |
req.headers | Request headers | req.headers.authorization |
res.json(data) | Send a JSON response | res.json({ name: "John" }) |
res.status(code) | Set the status code | res.status(201) |
res.send(text) | Send a text response | res.send("Hello!") |
res.end() | End the response without body | res.status(204).end() |
next(error) | Pass to next middleware/error | next(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