REST API Design Made Simple with Express.js

Search for a command to run...

No comments yet. Be the first to comment.
You open WhatsApp, type a message, and hit send. A single grey tick appears. You're on a train with no signal, but the message went through anyway. Or did it? That single tick is the beginning of a su

You record a Reel, add a song, trim it, and then close the app before posting. When you reopen Instagram, the draft is right there waiting. Nothing was lost. That experience feels simple. The system b

If you have built a React Native app, you have felt the pain of navigation setup. It is easy to get wrong and hard to keep clean as apps grow. This article explains what routing means, why it matters,

Modern apps are not just a pile of screens. They are a living system of features, data flows, offline behavior, and performance constraints. If you have only shipped small apps, the jump to something

Your application has user accounts. Some routes should only be accessible to logged-in users. How do you protect routes and verify that a request actually comes from who they claim to be? JWT (JSON We

Ashish's Blog
57 posts
APIs are how applications communicate. A REST API is a structured way to request and receive data over HTTP. Building REST APIs is one of the most common uses for Express.
An API (Application Programming Interface) is a contract between client and server. It says: "Send me this format, and I'll respond with that format."
For example, a social media API might say: "POST to /posts with a message, and I'll create a post."
REST (Representational State Transfer) is a style of designing APIs. It uses HTTP methods (GET, POST, PUT, DELETE) and URLs to represent operations on resources.
A resource is something you can create, read, update, or delete. Examples: users, posts, comments, products.
Instead of having endpoints like:
/createUser/getUserData/deleteUserREST uses the resource URL with HTTP methods:
GET /users - Get all usersPOST /users - Create a userGET /users/123 - Get user 123PUT /users/123 - Update user 123DELETE /users/123 - Delete user 123This is cleaner and more logical.
REST maps HTTP methods to database operations:
| HTTP Method | Operation | Meaning |
|---|---|---|
| GET | Read | Retrieve data |
| POST | Create | Add new data |
| PUT | Update | Modify existing data |
| DELETE | Delete | Remove data |
These four methods handle all basic operations (CRUD: Create, Read, Update, Delete).
Here's a simple REST API for managing blog posts:
import express from 'express';
const app = express();
app.use(express.json());
// Sample data
let posts = [
{ id: 1, title: 'First Post', content: 'Hello world' },
{ id: 2, title: 'Second Post', content: 'Another post' }
];
// GET all posts
app.get('/posts', (req, res) => {
res.json(posts);
});
// GET a single post
app.get('/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
});
// CREATE a new post
app.post('/posts', (req, res) => {
const newPost = {
id: posts.length + 1,
title: req.body.title,
content: req.body.content
};
posts.push(newPost);
res.status(201).json(newPost);
});
// UPDATE a post
app.put('/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
post.title = req.body.title || post.title;
post.content = req.body.content || post.content;
res.json(post);
});
// DELETE a post
app.delete('/posts/:id', (req, res) => {
const index = posts.findIndex(p => p.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'Post not found' });
}
const deleted = posts.splice(index, 1);
res.json({ message: 'Post deleted', post: deleted[0] });
});
app.listen(3000);
This API covers all CRUD operations for posts.
Follow these conventions for clean URLs:
/posts, /users, /products (not /post, /user)/posts (not /Posts)/blog-posts (not /blogposts or /blog_posts)GET /posts/getAllGET /postsUse appropriate HTTP status codes:
| Code | Meaning | Use When |
|---|---|---|
| 200 | OK | Request succeeded |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Client error (invalid data) |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Authenticated but not allowed |
| 404 | Not Found | Resource doesn't exist |
| 500 | Server Error | Server-side error |
app.get('/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.status(200).json(post); // 200 is default, can omit
});
app.post('/posts', (req, res) => {
const newPost = { id: posts.length + 1, ...req.body };
posts.push(newPost);
res.status(201).json(newPost);
});
Use query parameters for filtering and sorting:
app.get('/posts', (req, res) => {
let results = posts;
// Filter by status
if (req.query.status) {
results = results.filter(p => p.status === req.query.status);
}
// Sort by date
if (req.query.sort === 'date') {
results.sort((a, b) => new Date(b.date) - new Date(a.date));
}
// Pagination
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const start = (page - 1) * limit;
results = results.slice(start, start + limit);
res.json(results);
});
Usage:
GET /posts?status=publishedGET /posts?sort=dateGET /posts?page=2&limit=5Consistent error responses help clients:
app.get('/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) {
return res.status(404).json({
error: {
code: 'POST_NOT_FOUND',
message: 'The requested post does not exist',
details: `Post with ID ${req.params.id} not found`
}
});
}
res.json(post);
});
Structured error responses let clients handle errors properly.
Validate data before processing:
app.post('/posts', (req, res) => {
const { title, content } = req.body;
// Validation
if (!title || !content) {
return res.status(400).json({
error: 'Title and content are required'
});
}
if (title.length < 3) {
return res.status(400).json({
error: 'Title must be at least 3 characters'
});
}
const newPost = { id: posts.length + 1, title, content };
posts.push(newPost);
res.status(201).json(newPost);
});
As your API grows, you might need to change it. Versioning helps:
app.get('/v1/posts', (req, res) => {
// Version 1 implementation
res.json(posts);
});
app.get('/v2/posts', (req, res) => {
// Version 2 implementation (different format, more data, etc.)
res.json(posts.map(p => ({
id: p.id,
title: p.title,
description: p.content,
createdAt: new Date()
})));
});
This lets old clients continue using v1 while new clients use v2.
Protect routes with authentication:
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
// Verify token (simplified)
req.user = { id: 1 };
next();
}
app.post('/posts', authenticate, (req, res) => {
// Create post as authenticated user
const newPost = {
id: posts.length + 1,
...req.body,
userId: req.user.id
};
posts.push(newPost);
res.status(201).json(newPost);
});
app.delete('/posts/:id', authenticate, (req, res) => {
// Delete logic
});
Only authenticated users can create or delete.
If your API is called from a different domain, enable CORS:
npm install cors
import cors from 'cors';
app.use(cors());
// or restrict to specific domains
app.use(cors({
origin: 'https://example.com'
}));
Here's a realistic REST API with authentication, validation, and proper status codes:
import express from 'express';
const app = express();
app.use(express.json());
let posts = [];
let nextId = 1;
// Middleware: log requests
app.use((req, res, next) => {
console.log(`\({req.method} \){req.path}`);
next();
});
// Middleware: authenticate
function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (token === 'valid-token') {
req.user = { id: 1 };
next();
} else {
res.status(401).json({ error: 'Unauthorized' });
}
}
// GET all posts
app.get('/posts', (req, res) => {
res.json(posts);
});
// GET single post
app.get('/posts/:id', (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) return res.status(404).json({ error: 'Not found' });
res.json(post);
});
// POST create post (requires auth)
app.post('/posts', authenticate, (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ error: 'Title and content required' });
}
const newPost = { id: nextId++, title, content, userId: req.user.id };
posts.push(newPost);
res.status(201).json(newPost);
});
// PUT update post
app.put('/posts/:id', authenticate, (req, res) => {
const post = posts.find(p => p.id === parseInt(req.params.id));
if (!post) return res.status(404).json({ error: 'Not found' });
post.title = req.body.title || post.title;
post.content = req.body.content || post.content;
res.json(post);
});
// DELETE post
app.delete('/posts/:id', authenticate, (req, res) => {
const index = posts.findIndex(p => p.id === parseInt(req.params.id));
if (index === -1) return res.status(404).json({ error: 'Not found' });
posts.splice(index, 1);
res.json({ message: 'Deleted' });
});
app.listen(3000);
/posts not /getPostsBuild REST APIs following these principles, and you'll create clean, intuitive, maintainable APIs that developers love to use.