REST API Design Made Simple with Express.js

REST API is a standard way for the client to communicate with the server. Instead of mysterious endpoints like /getUser or /fetchUserData, REST uses predictable HTTP methods and resource-based URLs. Once you understand REST, building APIs becomes straightforward.
This is about understanding REST principles and building proper APIs with Express.
What REST API Means
REST stands for Representational State Transfer. It's a set of rules for building APIs.
Simple Definition
REST is a standard way to structure requests and responses between client and server using HTTP methods and resource URLs.
The Communication Problem
Without REST, APIs are chaotic:
GET /getUser?id=1
POST /fetchUserData
PUT /updateUserInfo
DELETE /removeUser?userId=1
GET /getAllUsers
Different names for similar operations. Confusing.
With REST, APIs are predictable:
GET /users/1 (get user)
POST /users (create user)
PUT /users/1 (update user)
DELETE /users/1 (delete user)
GET /users (get all users)
Same resource, different methods. Crystal clear.
Real Analogy
Think of a restaurant's menu (API):
Without REST:
bringSoup
bringBread
removePlate
giveBill
bringsalad
Confusing. No pattern.
With REST:
Action: BRING, Resource: SOUP
Action: BRING, Resource: BREAD
Action: REMOVE, Resource: PLATE
Action: GIVE, Resource: BILL
Clear pattern. Easy to understand.
Why REST Matters
REST makes APIs:
- Predictable (you can guess the URL)
- Consistent (same patterns everywhere)
- Easy to document
- Easy for clients to use
- Standard across the web
Resources in REST Architecture
Resources are the things your API manages.
What is a Resource?
A resource is anything your API handles: users, posts, comments, products, orders.
Resource URLs
Resources are accessed via URLs:
/users (users resource)
/posts (posts resource)
/comments (comments resource)
/products (products resource)
Specific Resources
Access specific resources with IDs:
/users/1 (user with ID 1)
/posts/42 (post with ID 42)
/comments/100 (comment with ID 100)
Nested Resources
Related resources can be nested:
/users/1/posts (posts by user 1)
/posts/42/comments (comments on post 42)
/users/1/posts/5/comments (comments on post 5 by user 1)
Resource Collections vs Specific Resources
/users (collection: all users)
/users/1 (specific: user with ID 1)
/posts (collection: all posts)
/posts/42 (specific: post with ID 42)
Naming Conventions
Use plural nouns for resources:
✓ /users (correct)
✗ /user (incorrect)
✓ /posts (correct)
✗ /post (incorrect)
✓ /comments (correct)
✗ /comment (incorrect)
Use lowercase and hyphens for multi-word resources:
✓ /user-profiles
✓ /blog-posts
✓ /shopping-carts
✗ /userProfiles (camelCase)
✗ /BlogPosts (PascalCase)
HTTP Methods: GET, POST, PUT, DELETE
HTTP methods tell the server what action to perform on a resource.
GET: Retrieve Data
GET retrieves existing data. Safe and doesn't change anything.
GET /users → Get all users
GET /users/1 → Get user with ID 1
GET /users/1/posts → Get posts by user 1
Code Example: GET
const express = require("express");
const app = express();
// Get all users
app.get("/users", (req, res) => {
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
res.json(users);
});
// Get specific user
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
const user = { id: userId, name: "Alice" };
res.json(user);
});
POST: Create Data
POST creates new resources.
POST /users → Create new user
POST /posts → Create new post
POST /users/1/posts → Create post for user 1
The request body contains the data to create.
Code Example: POST
app.use(express.json());
// Create new user
app.post("/users", (req, res) => {
const newUser = {
id: 3,
name: req.body.name,
email: req.body.email
};
res.status(201).json(newUser);
});
Test with curl:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Charlie","email":"charlie@example.com"}'
PUT: Update Data
PUT updates entire resources.
PUT /users/1 → Update user 1
PUT /posts/42 → Update post 42
The request body contains the updated data.
Code Example: PUT
// Update entire user
app.put("/users/:id", (req, res) => {
const userId = req.params.id;
const updatedUser = {
id: userId,
name: req.body.name,
email: req.body.email,
age: req.body.age
};
res.json(updatedUser);
});
Test with curl:
curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Alice Updated","email":"alice.new@example.com","age":26}'
PATCH: Partial Update
PATCH updates specific fields (not full replacement).
PATCH /users/1 → Update some fields of user 1
Code Example: PATCH
// Update specific fields
app.patch("/users/:id", (req, res) => {
const userId = req.params.id;
// Only update provided fields
const updates = {
id: userId,
name: req.body.name || "Alice", // Keep existing if not provided
email: req.body.email || "alice@example.com"
};
res.json(updates);
});
DELETE: Remove Data
DELETE removes resources.
DELETE /users/1 → Delete user 1
DELETE /posts/42 → Delete post 42
Code Example: DELETE
// Delete user
app.delete("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ message: `User ${userId} deleted` });
});
Test with curl:
curl -X DELETE http://localhost:3000/users/1
CRUD vs HTTP Methods
CRUD Operation HTTP Method Example
─────────────────────────────────────────────
Create POST POST /users
Read GET GET /users/1
Update PUT/PATCH PUT /users/1
Delete DELETE DELETE /users/1
Status Codes Basics
Status codes tell the client if the request succeeded or failed.
2xx: Success
Request was successful.
200 OK - Request succeeded
201 Created - Resource created
204 No Content - Success, no data to return
4xx: Client Error
Something wrong with the request.
400 Bad Request - Invalid request data
401 Unauthorized - Not authenticated
403 Forbidden - Authenticated but not allowed
404 Not Found - Resource doesn't exist
5xx: Server Error
Something wrong on the server.
500 Internal Server Error - Server error
503 Service Unavailable - Server down
Using Status Codes
app.post("/users", (req, res) => {
if (!req.body.email) {
return res.status(400).json({ error: "Email required" });
}
const newUser = { id: 1, name: req.body.name };
res.status(201).json(newUser); // 201 Created
});
app.get("/users/:id", (req, res) => {
const user = findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
res.status(200).json(user); // 200 OK
});
Common Status Codes
GET request → 200 OK
POST success → 201 Created
No content to show → 204 No Content
Invalid request → 400 Bad Request
Not authenticated → 401 Unauthorized
Resource not found → 404 Not Found
Server error → 500 Internal Server Error
Designing Routes Using REST Principles
Users Resource Example
GET /users → Get all users
GET /users/1 → Get user 1
POST /users → Create new user
PUT /users/1 → Update user 1
PATCH /users/1 → Partially update user 1
DELETE /users/1 → Delete user 1
Posts Resource Example
GET /posts → Get all posts
GET /posts/42 → Get post 42
POST /posts → Create new post
PUT /posts/42 → Update post 42
DELETE /posts/42 → Delete post 42
Nested Resources
User's posts:
GET /users/1/posts → Get posts by user 1
POST /users/1/posts → Create post for user 1
GET /users/1/posts/5 → Get post 5 by user 1
DELETE /users/1/posts/5 → Delete post 5 by user 1
Complete API Structure
Users
├─ GET /users (list all)
├─ POST /users (create)
├─ GET /users/1 (get one)
├─ PUT /users/1 (update)
└─ DELETE /users/1 (delete)
Posts
├─ GET /posts (list all)
├─ POST /posts (create)
├─ GET /posts/1 (get one)
├─ PUT /posts/1 (update)
└─ DELETE /posts/1 (delete)
User's Posts
├─ GET /users/1/posts (user's posts)
└─ POST /users/1/posts (user creates post)
REST Request-Response Lifecycle
Client sends request
├─ Method: POST
├─ URL: /users
└─ Body: { name: "Alice", email: "alice@example.com" }
|
v
Server receives request
|
v
Server validates data
├─ Valid? → Continue
└─ Invalid? → Return 400 error
|
v
Server creates resource
├─ Success? → Continue
└─ Error? → Return 500 error
|
v
Server sends response
├─ Status: 201 Created
└─ Body: { id: 1, name: "Alice", email: "alice@example.com" }
|
v
Client receives response
|
v
Client displays data or error
Example Resource: Users
Building a complete users API.
Simple Users API
const express = require("express");
const app = express();
app.use(express.json());
// In-memory database
let users = [
{ id: 1, name: "Alice", email: "alice@example.com" },
{ id: 2, name: "Bob", email: "bob@example.com" }
];
let nextId = 3;
// GET all users
app.get("/users", (req, res) => {
res.json(users);
});
// GET specific 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({ error: "User not found" });
}
res.json(user);
});
// POST create user
app.post("/users", (req, res) => {
// Validate
if (!req.body.name || !req.body.email) {
return res.status(400).json({ error: "Name and email required" });
}
// Create
const newUser = {
id: nextId++,
name: req.body.name,
email: req.body.email
};
users.push(newUser);
// Return 201 Created
res.status(201).json(newUser);
});
// PUT update user
app.put("/users/:id", (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Validate
if (!req.body.name || !req.body.email) {
return res.status(400).json({ error: "Name and email required" });
}
// Update
user.name = req.body.name;
user.email = req.body.email;
res.json(user);
});
// PATCH partial update
app.patch("/users/:id", (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: "User not found" });
}
// Update only provided fields
if (req.body.name) user.name = req.body.name;
if (req.body.email) user.email = req.body.email;
res.json(user);
});
// DELETE user
app.delete("/users/:id", (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: "User not found" });
}
const deletedUser = users.splice(index, 1);
res.json({ message: "User deleted", user: deletedUser[0] });
});
app.listen(3000, () => {
console.log("API running on http://localhost:3000");
});
Testing the API
Get all users:
curl http://localhost:3000/users
# [{"id":1,"name":"Alice","email":"alice@example.com"},{"id":2,"name":"Bob","email":"bob@example.com"}]
Get one user:
curl http://localhost:3000/users/1
# {"id":1,"name":"Alice","email":"alice@example.com"}
Create user:
curl -X POST http://localhost:3000/users \
-H "Content-Type: application/json" \
-d '{"name":"Charlie","email":"charlie@example.com"}'
# {"id":3,"name":"Charlie","email":"charlie@example.com"}
Update user:
curl -X PUT http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"name":"Alice Updated","email":"alice.new@example.com"}'
# {"id":1,"name":"Alice Updated","email":"alice.new@example.com"}
Partial update:
curl -X PATCH http://localhost:3000/users/1 \
-H "Content-Type: application/json" \
-d '{"email":"newemail@example.com"}'
# {"id":1,"name":"Alice Updated","email":"newemail@example.com"}
Delete user:
curl -X DELETE http://localhost:3000/users/1
# {"message":"User deleted","user":{"id":1,"name":"Alice Updated","email":"newemail@example.com"}}
API Request-Response Examples
GET Request
Request:
────────
GET /users/1 HTTP/1.1
Host: localhost:3000
Response:
─────────
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1,
"name": "Alice",
"email": "alice@example.com"
}
POST Request
Request:
────────
POST /users HTTP/1.1
Host: localhost:3000
Content-Type: application/json
{
"name": "Charlie",
"email": "charlie@example.com"
}
Response:
─────────
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 3,
"name": "Charlie",
"email": "charlie@example.com"
}
Error Response
Request:
────────
GET /users/999 HTTP/1.1
Host: localhost:3000
Response:
─────────
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": "User not found"
}
Best Practices for REST APIs
Use Correct HTTP Methods
// ✓ Correct
GET /users (retrieve)
POST /users (create)
PUT /users/1 (update)
DELETE /users/1 (delete)
// ✗ Wrong
GET /deleteUser?id=1 (wrong method)
POST /getUsers (should be GET)
GET /updateUser?id=1 (wrong method)
Use Correct Status Codes
// ✓ Correct
app.post("/users", (req, res) => {
res.status(201).json(newUser); // 201 for create
});
app.get("/users/:id", (req, res) => {
if (!user) {
return res.status(404).json({ error: "Not found" }); // 404 for missing
}
res.json(user); // 200 for success
});
// ✗ Wrong
app.post("/users", (req, res) => {
res.json(newUser); // No status (defaults to 200, should be 201)
});
Use Plural Nouns for Resources
// ✓ Correct
GET /users
POST /posts
DELETE /comments/1
// ✗ Wrong
GET /user
POST /post
DELETE /comment/1
Use Logical Nesting
// ✓ Correct (related resources)
GET /users/1/posts (user's posts)
// ✗ Wrong (too deeply nested)
GET /users/1/posts/2/comments/3/author/replies
Validate Input
app.post("/users", (req, res) => {
// ✓ Validate
if (!req.body.name || !req.body.email) {
return res.status(400).json({ error: "Invalid data" });
}
// Process...
});
Practice Assignment
1. Plan a products API:
// Design routes for products resource
// GET /products
// GET /products/:id
// POST /products
// PUT /products/:id
// DELETE /products/:id
// Write route signatures
2. Build simple users API:
// Create Express server
// Implement all CRUD operations for users
// Test each endpoint with curl
// Return correct status codes
3. Add validation:
// Add input validation to POST /users
// Check required fields
// Return 400 for invalid data
// Test with valid and invalid requests
4. Nested resources:
// Create users and posts resources
// Implement GET /users/:id/posts
// Get all posts for a specific user
// Test the nested route
5. Error handling:
// Add proper error handling
// Return 404 for missing resources
// Return 400 for bad requests
// Return 500 for server errors
// Test error scenarios
Quick Recap
REST is a standard for building predictable, consistent APIs.
Resources are things your API manages: users, posts, products.
HTTP methods tell the server what action to perform: GET, POST, PUT, DELETE.
GET retrieves data without changing anything.
POST creates new resources.
PUT updates entire resources.
PATCH updates specific fields.
DELETE removes resources.
Status codes tell the client if the request succeeded or failed.
2xx codes indicate success (200 OK, 201 Created).
4xx codes indicate client errors (400 Bad Request, 404 Not Found).
5xx codes indicate server errors (500 Internal Server Error).
Use plural nouns for resource names:
/users, not/user.Use lowercase and hyphens for multi-word resources:
/user-profiles.Collection endpoints return all resources:
GET /users.Specific endpoints use IDs:
GET /users/1.Nested resources show relationships:
GET /users/1/posts.Request validation prevents bad data from being saved.
Proper status codes help clients understand what happened.
Consistent naming makes APIs predictable and easy to use.
REST design makes APIs intuitive even for new users.
REST APIs are the standard for building modern web services.
If you enjoyed this article, check out my other blogs on this profile. Connect with me: LinkedIn | GitHub | X (Twitter)



