REST API Design Made Simple with Express.js
Every application you use daily is talking to a server somewhere. When you open Instagram, your phone asks a server for your feed. When you place an order on Swiggy, your app sends your order details to a server. When you update your LinkedIn profile, your changes are sent to a server that stores them. All of this communication between your app and the server, happens through an API.
An API, or Application Programming Interface, is simply an agreed-upon way for two systems to talk to each other. Think of it like a waiter in a restaurant. You do not walk into the kitchen and cook your own food. You tell the waiter what you want, the waiter communicates it to the kitchen, and the kitchen sends back what you asked for. The waiter is the API, the defined interface between you and the system that has what you need.
REST is a specific style of designing these APIs, and it is by far the most widely used approach on the web today. Understanding REST is not just about learning a set of rules, it is about understanding a way of thinking about how servers should expose their data and functionality to the outside world.
What REST Actually Means ?
REST stands for Representational State Transfer. That sounds academic, but the idea behind it is practical and elegant. REST is built on one core concept: everything on your server is a resource, and your API gives clients a standardized way to interact with those resources.
A resource is anything meaningful that your server manages: users, orders, products, posts, messages. Each resource has its own URL, called an endpoint. Clients interact with resources by sending HTTP requests to those endpoints using standard HTTP methods. The server responds with the current state of the resource in a format both sides understand, almost always JSON in modern APIs.
What makes REST powerful is that it is stateless. Every request from a client contains all the information the server needs to process it. The server does not remember previous requests. Each request stands completely on its own. This makes REST APIs predictable, scalable, and easy to reason about.
Before REST became the standard, APIs were designed inconsistently. One API might use /getUser to fetch a user and /deleteUser to delete one. Another might use /user/fetch and /user/remove. Every API was its own convention that developers had to learn from scratch. REST replaced all of that chaos with a uniform interface that anyone familiar with HTTP already understands.
Resources and How to Name Them
The first step in designing a REST API is identifying your resources and giving them clean, consistent URLs. REST has a clear convention for this, and following it makes your API immediately intuitive to anyone who has worked with REST before.
Resources are always named as nouns, never verbs. The URL identifies what you are working with, and the HTTP method tells the server what you want to do with it. This separation of resource identity and action is fundamental.
Resources are also always plural. Not /user but /users. Not /product but /products. This makes the URL consistent regardless of whether you are working with one item or many.
For a users resource, the URLs follow this pattern:
/users — the collection of all users
/users/:id — a specific user identified by their ID
Everything you do with users happens through these two URLs. You do not create /getUsers, /createUser, /updateUser, or /deleteUser. The URL stays the same , what changes is the HTTP method you use.
HTTP Methods — The Verbs of REST
HTTP methods are how REST expresses intent. The URL says what resource you are working with, and the method says what you want to do with it. There are four methods that cover everything you will ever need to do with a resource, and they map to the classic CRUD operations - Create, Read, Update, Delete.
GET — Read Data
GET is used to retrieve data. It never modifies anything on the server. Calling GET on the same endpoint twice should give you the same result; it is a read-only operation. This property is called idempotency, and it matters because GET requests can be safely cached, retried, and bookmarked.
const express = require("express");
const app = express();
app.use(express.json());
// In-memory users array for demonstration
let users = [
{ id: 1, name: "Ram", email: "ram@example.com", city: "Hyderabad" },
{ id: 2, name: "Shiva", email: "shiva@example.com", city: "Bangalore" },
{ id: 3, name: "Priya", email: "priya@example.com", city: "Chennai" }
];
// GET all users
app.get("/users", (req, res) => {
res.status(200).json({
success: true,
count: users.length,
data: users
});
});
// GET a specific user by ID
app.get("/users/:id", (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({
success: false,
message: "User not found"
});
}
res.status(200).json({
success: true,
data: user
});
});
Two GET routes, one for the collection, one for a specific item. The :id in the route is a parameter that Express captures from the URL. If someone requests /users/2, req.params.id is "2".
POST — Create Data
POST is used to create a new resource. The client sends the data for the new resource in the request body, and the server creates it, assigns it an ID, and returns the newly created resource. Unlike GET, POST is not idempotent; sending the same POST request twice creates two separate resources.
// POST — create a new user
app.post("/users", (req, res) => {
const { name, email, city } = req.body;
if (!name || !email || !city) {
return res.status(400).json({
success: false,
message: "Name, email and city are required"
});
}
const newUser = {
id: users.length + 1,
name,
email,
city
};
users.push(newUser);
res.status(201).json({
success: true,
message: "User created successfully",
data: newUser
});
});
Notice the status code is 201 — Created not 200. This distinction matters and we will come back to it shortly. Also notice the validation before creating the user. A well-designed API always validates incoming data and returns a clear error if something is missing or wrong.
PUT — Update Data
PUT is used to update an existing resource. The client sends the updated data in the request body, and the server replaces the existing resource with the new data. PUT is typically used for full updates, you send the entire resource, not just the fields that changed.
// PUT — update an existing user
app.put("/users/:id", (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: "User not found"
});
}
const { name, email, city } = req.body;
if (!name || !email || !city) {
return res.status(400).json({
success: false,
message: "Name, email and city are required for a full update"
});
}
users[userIndex] = {
id: parseInt(req.params.id),
name,
email,
city
};
res.status(200).json({
success: true,
message: "User updated successfully",
data: users[userIndex]
});
});
There is also a PATCH method, which is used for partial updates, only sending the fields you want to change. PUT replaces the whole resource, PATCH modifies parts of it. For a beginner-friendly API, PUT is enough to start with, and you can introduce PATCH later when the distinction becomes relevant.
DELETE — Remove Data
DELETE is used to remove a resource. The client sends a request to the specific resource's URL, and the server deletes it. After a successful delete, the resource no longer exists, and requesting it again should return a 404.
// DELETE — remove a user
app.delete("/users/:id", (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) {
return res.status(404).json({
success: false,
message: "User not found"
});
}
const deletedUser = users[userIndex];
users.splice(userIndex, 1);
res.status(200).json({
success: true,
message: "User deleted successfully",
data: deletedUser
});
});
Status Codes : Speaking HTTP Properly
Status codes are how the server communicates the outcome of a request. Every HTTP response includes a three-digit status code that tells the client whether the request succeeded, failed, or something else happened. Using the right status code is not just good practice, it is how other developers and client applications know how to handle your API's responses.
Status codes are grouped into ranges, and understanding the ranges is more important than memorizing every code:
2xx — Success. The request worked. The most common ones you will use are 200 OK for a successful GET, PUT, or DELETE, and 201 Created for a successful POST that created a new resource.
4xx — Client Error. The request failed because of something the client did wrong. The most common ones are 400 Bad Request when the client sent invalid or missing data, 401 Unauthorized when authentication is required, 403 Forbidden when the client is authenticated but does not have permission, and 404 Not Found when the requested resource does not exist.
5xx — Server Error. The request failed because something went wrong on the server; not the client's fault. 500 Internal Server Error is the generic catch-all for unexpected server failures.
Here is how these map to the users API you just built:
GET /users → 200 OK
GET /users/1 → 200 OK
GET /users/999 → 404 Not Found
POST /users → 201 Created
POST /users (missing fields) → 400 Bad Request
PUT /users/1 → 200 OK
PUT /users/999 → 404 Not Found
DELETE /users/1 → 200 OK
DELETE /users/999 → 404 Not Found
Using status codes correctly means the clients consuming your API can handle responses programmatically without parsing the response body to figure out if something went wrong. A 404 immediately tells the client "that thing does not exist." A 400 immediately tells the client "you sent bad data." A 201 immediately tells the client "something was created." This is why consistent, correct status codes are a mark of a well-designed API.
Putting the Full API Together
Here is the complete users API with all four methods, validation, and proper status codes in one place:
const express = require("express");
const app = express();
app.use(express.json());
let users = [
{ id: 1, name: "Ram", email: "ram@example.com", city: "Hyderabad" },
{ id: 2, name: "Shiva", email: "shiva@example.com", city: "Bangalore" },
{ id: 3, name: "Priya", email: "priya@example.com", city: "Chennai" }
];
// GET all users
app.get("/users", (req, res) => {
res.status(200).json({ success: true, count: users.length, data: 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({ success: false, message: "User not found" });
res.status(200).json({ success: true, data: user });
});
// POST create user
app.post("/users", (req, res) => {
const { name, email, city } = req.body;
if (!name || !email || !city) {
return res.status(400).json({ success: false, message: "Name, email and city are required" });
}
const newUser = { id: users.length + 1, name, email, city };
users.push(newUser);
res.status(201).json({ success: true, message: "User created successfully", data: newUser });
});
// PUT update user
app.put("/users/:id", (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) return res.status(404).json({ success: false, message: "User not found" });
const { name, email, city } = req.body;
if (!name || !email || !city) {
return res.status(400).json({ success: false, message: "All fields required for update" });
}
users[userIndex] = { id: parseInt(req.params.id), name, email, city };
res.status(200).json({ success: true, message: "User updated", data: users[userIndex] });
});
// DELETE user
app.delete("/users/:id", (req, res) => {
const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
if (userIndex === -1) return res.status(404).json({ success: false, message: "User not found" });
const deleted = users.splice(userIndex, 1)[0];
res.status(200).json({ success: true, message: "User deleted", data: deleted });
});
app.listen(3000, () => console.log("Server running on port 3000"));
This is a complete, functional REST API for a users resource. It follows REST conventions, uses HTTP methods correctly, returns appropriate status codes, validates input, and responds with consistent JSON. Any developer familiar with REST could pick this up and immediately understand how to use it.
REST Conventions to Always Follow
Before wrapping up, a few conventions that distinguish a well-designed REST API from one that just works:
Use nouns for resource names, never verbs. /users is correct. /getUsers or /fetchUsers is not the verb is already expressed by the HTTP method. Keep URLs lowercase and use hyphens for multi-word resources , /blog-posts not /blogPosts or /BlogPosts. Always return consistent response shapes, if successful responses have a success and data field, every response should follow that structure. Never leave the client guessing about the format. Nest related resources sensibly, if you want to get all orders for a specific user, /users/:id/orders is more meaningful than a completely separate /orders endpoint with a query parameter.
Why REST Design Matters ?
A poorly designed API is one of the most frustrating things a developer can work with. Inconsistent URL naming, wrong status codes, unclear error messages, unpredictable response formats; all of these slow down the teams consuming your API and create bugs that are genuinely difficult to trace. A well-designed REST API, on the other hand, is self-documenting. A developer can look at your routes and immediately understand what they do, what they expect, and what they return.
REST is not a rigid specification with a compliance checklist. It is a set of principles that, when followed consistently, produce APIs that are intuitive, predictable, and easy to build on top of. The investment in learning these conventions pays back every time someone including future you needs to work with an API you designed.
