Hello readers 👋, welcome to the 9th blog in our Node.js series!
In our last post, we learned how JWT authentication keeps your application secure without the need for server-side sessions. Today, we're going to focus on something equally essential: how to design a clean, predictable, and intuitive API using Express.js following REST principles.
Whether you're building a mobile app, a frontend in React, or just exposing data to the world, you'll need an API that clients can understand and use easily. REST gives us a set of conventions that make APIs feel natural. We'll break down what a REST API is, map out HTTP methods, introduce status codes, and design routes for a simple users resource. By the end, you'll be able to structure your own Express.js APIs clearly.
Let's jump right in.
What is a REST API?
First, let's define API. An API (Application Programming Interface) is a set of rules that allows two pieces of software to talk to each other. When you use any app on your phone, it likely talks to a server using an API. The API is the contract between the client and the server: "you send me a request in this format, and I'll give you a response in that format."
REST stands for Representational State Transfer. It's an architectural style that uses the standard HTTP methods (GET, POST, PUT, DELETE) and follows a few guiding principles:
- Everything is a resource (like a user, a product, an article).
- Each resource is accessed via a unique URL (endpoint), like
/usersor/users/1. - The client and server are stateless: each request must contain all the information the server needs to process it, and the server doesn't keep any session state between requests.
- Operations on resources are performed using the standard HTTP methods.
That's it. No magic, just a disciplined way of using URLs and HTTP verbs.
Resources in REST architecture
A resource is the core concept. In our example, we'll have a users resource. Think of a user as an object that the API can create, read, update, or delete. The endpoints (URLs) for this resource follow a convention:
-
/users– the collection of users. -
/users/:id– a single user identified by a unique ID.
This consistent naming makes APIs intuitive. Even before you read the documentation, you can guess that GET /users returns all users, and GET /users/5 returns user #5.
HTTP methods and what they do
REST maps the basic CRUD operations (Create, Read, Update, Delete) to the following HTTP methods:
| HTTP Method | Operation | Example Endpoint | Meaning |
|---|---|---|---|
| GET | Read | GET /users |
Retrieve all users |
| GET | Read | GET /users/1 |
Retrieve a single user with ID 1 |
| POST | Create | POST /users |
Create a new user |
| PUT | Update (replace) | PUT /users/1 |
Replace the entire user with ID 1 |
| PATCH | Update (partial) | PATCH /users/1 |
Modify only some fields of user 1 |
| DELETE | Delete | DELETE /users/1 |
Remove user with ID 1 |
We'll focus on the most commonly used: GET, POST, PUT, and DELETE. Think of them as the verbs of your API.
Setting up the Express.js server
Let's quickly spin up a minimal Express server. If you haven't installed Express, run npm install express in your project folder.
const express = require('express');
const app = express();
app.use(express.json()); // to parse JSON request bodies
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
We'll add routes to this server.
Designing routes for the "users" resource
We'll create an in-memory array to hold user objects. Remember, in a real application you'd use a database, but the principles remain the same.
In-memory data store
let users = [
{ id: 1, name: 'Satya', email: 'satya@example.com' },
{ id: 2, name: 'Priya', email: 'priya@example.com' }
];
let nextId = 3; // to assign unique IDs
Now, let's implement each route following REST conventions.
1. GET /users – fetch all users
app.get('/users', (req, res) => {
res.json(users);
});
A simple GET request returns the entire users array with status 200 (OK, but it's implied in res.json).
2. GET /users/:id – fetch a single user
app.get('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const user = users.find(u => u.id === id);
if (!user) {
return res.status(404).json({ message: 'User not found' });
}
res.json(user);
});
Here, :id is a route parameter. If the user doesn't exist, we return a 404 Not Found status code.
3. POST /users – create a new user
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: 'Name and email are required' });
}
const newUser = { id: nextId++, name, email };
users.push(newUser);
res.status(201).json(newUser);
});
A POST request creates a new resource. We send a 201 Created status code, along with the newly created object. We also validate input: if name or email is missing, we respond with 400 Bad Request.
4. PUT /users/:id – replace a user entirely
app.put('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ message: 'Name and email are required' });
}
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ message: 'User not found' });
}
// Replace the entire object
users[index] = { id, name, email };
res.json(users[index]);
});
PUT typically expects a complete replacement. If the user doesn't exist, we return 404. On success, we return the updated user with 200.
5. DELETE /users/:id – remove a user
app.delete('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) {
return res.status(404).json({ message: 'User not found' });
}
users.splice(index, 1);
res.status(204).send(); // No Content
});
DELETE successfully returns a 204 No Content status with an empty body, indicating that the operation was successful and there's nothing else to send.
Status codes basics
HTTP status codes tell the client what happened. Here are the basics:
| Code | Meaning | When to use |
|---|---|---|
| 200 | OK | Successful GET or PUT request |
| 201 | Created | Resource created successfully (POST) |
| 204 | No Content | Successful DELETE, nothing to return |
| 400 | Bad Request | Client sent invalid data (missing fields, wrong format) |
| 401 | Unauthorized | Authentication is missing or has failed |
| 404 | Not Found | Requested resource doesn't exist |
| 500 | Internal Server Error | Something went wrong on the server side |
Always return the appropriate status code so the client can handle the response correctly. It's part of the contract.
The request-response lifecycle
When a client makes a request, here's what happens:
- The client sends an HTTP request to a specific endpoint with a method, headers, and optionally a body.
- The Express server receives the request and matches it to the defined route.
- The route handler reads parameters, query strings, and body; performs business logic; and sends back a response with a status code and data.
- The client receives the response and updates its UI or state.
For example, a POST /users with { "name": "Rahul", "email": "rahul@example.com" } triggers the creation of a new user and the 201 response.
Clean route naming conventions
When designing a REST API:
- Use plural nouns for collections:
/users,/posts,/products. This is conventional and predictable. - Use hierarchical routes for nested resources:
/users/1/poststo get posts of user #1. But don't over-nest; keep it flat if possible. - Use query parameters for filtering, sorting, and pagination:
GET /users?role=admin&limit=10. - Prefer kebab-case or lowercase:
/user-profiles(though many prefer just nouns and sub-resources).
Putting it all together: a complete Express API for users
Here's the full code for a simple, working REST API:
const express = require('express');
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: 'Satya', email: 'satya@example.com' },
{ id: 2, name: 'Priya', email: 'priya@example.com' }
];
let nextId = 3;
// GET all users
app.get('/users', (req, res) => {
res.json(users);
});
// GET single user
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) return res.status(404).json({ message: 'User not found' });
res.json(user);
});
// POST create user
app.post('/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) return res.status(400).json({ message: 'Name and email required' });
const newUser = { id: nextId++, name, email };
users.push(newUser);
res.status(201).json(newUser);
});
// PUT replace user
app.put('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const { name, email } = req.body;
if (!name || !email) return res.status(400).json({ message: 'Name and email required' });
const index = users.findIndex(u => u.id === id);
if (index === -1) return res.status(404).json({ message: 'User not found' });
users[index] = { id, name, email };
res.json(users[index]);
});
// DELETE user
app.delete('/users/:id', (req, res) => {
const id = parseInt(req.params.id);
const index = users.findIndex(u => u.id === id);
if (index === -1) return res.status(404).json({ message: 'User not found' });
users.splice(index, 1);
res.status(204).send();
});
app.listen(3000, () => console.log('Server running on port 3000'));
You can test these endpoints with Postman, curl, or your browser (for GET requests). The API is self-descriptive and follows REST conventions.
Here's a quick reference table:
| CRUD Operation | HTTP Method | Endpoint | Example |
|---|---|---|---|
| Create | POST | /users |
POST /users (with body) |
| Read (all) | GET | /users |
GET /users |
| Read (one) | GET | /users/:id |
GET /users/1 |
| Update | PUT | /users/:id |
PUT /users/1 (with body) |
| Delete | DELETE | /users/:id |
DELETE /users/1 |
Conclusion
A well-designed REST API makes your backend predictable and easy for any client to consume. Using Express.js, you can quickly map HTTP methods to resource actions, keep your routes clean, and respond with proper status codes. This forms the foundation of any full-stack application.
To recap the key points:
- A REST API uses standard HTTP methods to perform operations on resources identified by URLs.
- Resources are nouns; endpoints like
/usersrepresent collections, and/users/:idrepresent individual items. - GET retrieves data, POST creates, PUT replaces, and DELETE removes resources.
- Status codes (200, 201, 204, 400, 404, etc.) communicate the result clearly.
- Clean route naming and appropriate status codes make your API intuitive and easy to maintain.
- Express.js provides a minimal and flexible way to build such APIs.
Now you're equipped to design your own RESTful APIs from scratch. In the next blog, we'll continue building on this foundation, perhaps adding real database storage or authentication to protect routes. See you there!
Hope you found this helpful! If you spot any mistakes or have suggestions, let me know. You can find me on LinkedIn and X, where I post more about web development.
Top comments (0)