REST API Design Made Simple with Express.js

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.
What is an API?
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."
What is REST?
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/deleteUser
REST 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 123
This is cleaner and more logical.
HTTP Methods for CRUD
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).
Basic REST API Example
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.
Naming Conventions
Follow these conventions for clean URLs:
- Use plural nouns for resources:
/posts,/users,/products(not/post,/user) - Use lowercase:
/posts(not/Posts) - Use hyphens for multi-word resources:
/blog-posts(not/blogpostsor/blog_posts) - Avoid verbs in URLs: Use HTTP methods instead
- Wrong:
GET /posts/getAll - Right:
GET /posts
- Wrong:
Status Codes
Use 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);
});
Handling Query Parameters
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=5
Error Handling
Consistent 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.
Request Validation
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);
});
API Versioning
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.
Authentication in REST APIs
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.
CORS for Cross-Origin Requests
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'
}));
Complete REST API Example
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);
Key Takeaways
- REST uses HTTP methods (GET, POST, PUT, DELETE) to represent operations
- Resources are nouns, not verbs:
/postsnot/getPosts - Status codes indicate operation success: 200, 201, 400, 404, 500
- Query parameters filter and sort results
- Validation catches bad data before processing
- Authentication protects sensitive operations
- Consistent error responses help clients handle errors
- Versioning lets you evolve your API without breaking clients
Build REST APIs following these principles, and you'll create clean, intuitive, maintainable APIs that developers love to use.



