3 Best Examples of Creating a RESTful API with Express.js
Most tutorials start with definitions. Let’s skip that and jump straight into examples of creating a RESTful API with Express.js: 3 examples that show different levels of complexity:
- A Notes API using in-memory storage (great for learning and quick prototypes)
- A User API with validation and JWT authentication
- A Products API backed by a database-style layer and modular structure
All examples use modern JavaScript (ES modules or CommonJS-style syntax you can adapt), async/await, and Express 4+. You can run these with Node 18+ in 2024–2025 without extra transpilers.
Example 1: Minimal Notes API – The Fastest Way to Learn Express REST
This first example of creating a RESTful API with Express.js keeps everything in memory. It’s perfect when you want to:
- Understand routing and HTTP verbs
- Try out tools like Postman or curl
- Teach REST basics to a teammate or in a workshop
Core features in this example
This first of our 3 examples includes:
GET /notes– list all notesGET /notes/:id– get a single notePOST /notes– create a notePUT /notes/:id– update a noteDELETE /notes/:id– remove a note
Here’s the complete Express.js server in one file:
// notes-api.js
const express = require('express');
const app = express();
app.use(express.json());
let notes = [
{ id: 1, title: 'First note', content: 'Hello, Express!' },
{ id: 2, title: 'Second note', content: 'REST APIs are fun.' }
];
let nextId = 3;
// GET /notes - list all notes
app.get('/notes', (req, res) => {
res.json(notes);
});
// GET /notes/:id - get one note
app.get('/notes/:id', (req, res) => {
const id = Number(req.params.id);
const note = notes.find(n => n.id === id);
if (!note) {
return res.status(404).json({ error: 'Note not found' });
}
res.json(note);
});
// POST /notes - create a note
app.post('/notes', (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ error: 'title and content are required' });
}
const newNote = { id: nextId++, title, content };
notes.push(newNote);
res.status(201).json(newNote);
});
// PUT /notes/:id - update a note
app.put('/notes/:id', (req, res) => {
const id = Number(req.params.id);
const { title, content } = req.body;
const noteIndex = notes.findIndex(n => n.id === id);
if (noteIndex === -1) {
return res.status(404).json({ error: 'Note not found' });
}
if (!title || !content) {
return res.status(400).json({ error: 'title and content are required' });
}
const updatedNote = { id, title, content };
notes[noteIndex] = updatedNote;
res.json(updatedNote);
});
// DELETE /notes/:id - delete a note
app.delete('/notes/:id', (req, res) => {
const id = Number(req.params.id);
const existingLength = notes.length;
notes = notes.filter(n => n.id !== id);
if (notes.length === existingLength) {
return res.status(404).json({ error: 'Note not found' });
}
res.status(204).send();
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Notes API listening on port ${PORT}`);
});
This first example of creating a RESTful API with Express.js shows the bare minimum you need for a usable CRUD service. It’s not production-ready, but it’s a perfect sandbox for learning how REST endpoints map to HTTP verbs.
Example 2: User API with Validation, JWT Auth, and Better Error Handling
The second of our 3 best examples of creating a RESTful API with Express.js adds features you actually need in production:
- Input validation
- Password hashing
- JWT-based authentication
- Centralized error handling middleware
This example of creating a RESTful API with Express.js is closer to what you’d see in a real SaaS backend, just simplified to fit in an article.
Features included in this User API
This example includes:
POST /auth/register– register a new userPOST /auth/login– log in and receive a JWTGET /me– get the current user profile (requires JWT)
For hashing and JWTs, install:
npm install express bcrypt jsonwebtoken
Then create a file like user-api.js:
// user-api.js
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
// In-memory store for demo purposes only
const users = [];
const JWT_SECRET = process.env.JWT_SECRET || 'change_this_in_production';
// Simple validation middleware
function validateRegistration(req, res, next) {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'email and password are required' });
}
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
return res.status(400).json({ error: 'invalid email format' });
}
if (password.length < 8) {
return res.status(400).json({ error: 'password must be at least 8 characters' });
}
next();
}
// Auth middleware
function authRequired(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'missing or invalid Authorization header' });
}
const token = authHeader.split(' ')[1];
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'invalid or expired token' });
}
}
// Register
app.post('/auth/register', validateRegistration, async (req, res, next) => {
try {
const { email, password } = req.body;
const existing = users.find(u => u.email === email);
if (existing) {
return res.status(409).json({ error: 'email already registered' });
}
const passwordHash = await bcrypt.hash(password, 10);
const user = { id: users.length + 1, email, passwordHash };
users.push(user);
res.status(201).json({ id: user.id, email: user.email });
} catch (err) {
next(err);
}
});
// Login
app.post('/auth/login', async (req, res, next) => {
try {
const { email, password } = req.body;
const user = users.find(u => u.email === email);
if (!user) {
return res.status(401).json({ error: 'invalid credentials' });
}
const passwordMatches = await bcrypt.compare(password, user.passwordHash);
if (!passwordMatches) {
return res.status(401).json({ error: 'invalid credentials' });
}
const token = jwt.sign({ userId: user.id, email: user.email }, JWT_SECRET, {
expiresIn: '1h'
});
res.json({ token });
} catch (err) {
next(err);
}
});
// Get current user
app.get('/me', authRequired, (req, res) => {
const user = users.find(u => u.id === req.user.userId);
if (!user) {
return res.status(404).json({ error: 'user not found' });
}
res.json({ id: user.id, email: user.email });
});
// Centralized error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: 'internal server error' });
});
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`User API listening on port ${PORT}`);
});
This second example of creating a RESTful API with Express.js illustrates patterns that data-heavy services use in 2024–2025: validation middleware, JWT auth, and central error handling. If you’re building anything with user accounts, this is the pattern you’ll reuse again and again.
For a deeper background on password security and hashing, the National Institute of Standards and Technology (NIST) provides widely referenced guidance on digital identity at nist.gov.
Example 3: Products API with Modular Routing and a Data Layer
The third of our 3 best examples of creating a RESTful API with Express.js focuses on structure. It simulates a small e-commerce-style Products API with:
- Modular routers
- A separate data access layer
- Query parameters for filtering and pagination
- Reusable error utilities
In real projects, this is closer to how teams organize code so it doesn’t collapse under its own weight.
Project structure for this example
Instead of one big file, split the app like this:
products-api/
app.js
routes/
products.js
data/
products-store.js
utils/
errors.js
utils/errors.js
// utils/errors.js
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
class BadRequestError extends Error {
constructor(message) {
super(message);
this.name = 'BadRequestError';
this.statusCode = 400;
}
}
module.exports = { NotFoundError, BadRequestError };
data/products-store.js
In a real app this would wrap a database like PostgreSQL or MongoDB. Here we fake it with an array but keep the interface asynchronous so you can swap in a real DB later.
// data/products-store.js
const { NotFoundError } = require('../utils/errors');
let products = [
{ id: 1, name: 'Laptop', price: 1299.99, inStock: true },
{ id: 2, name: 'Mechanical Keyboard', price: 149.99, inStock: true },
{ id: 3, name: 'Wireless Mouse', price: 59.99, inStock: false }
];
let nextId = 4;
async function list({ inStock, minPrice, maxPrice, limit = 20, offset = 0 }) {
let result = [...products];
if (typeof inStock === 'boolean') {
result = result.filter(p => p.inStock === inStock);
}
if (typeof minPrice === 'number') {
result = result.filter(p => p.price >= minPrice);
}
if (typeof maxPrice === 'number') {
result = result.filter(p => p.price <= maxPrice);
}
return result.slice(offset, offset + limit);
}
async function getById(id) {
const product = products.find(p => p.id === id);
if (!product) throw new NotFoundError('Product not found');
return product;
}
async function create({ name, price, inStock = true }) {
const product = { id: nextId++, name, price, inStock };
products.push(product);
return product;
}
async function update(id, { name, price, inStock }) {
const index = products.findIndex(p => p.id === id);
if (index === -1) throw new NotFoundError('Product not found');
const existing = products[index];
const updated = {
...existing,
...(name !== undefined ? { name } : {}),
...(price !== undefined ? { price } : {}),
...(inStock !== undefined ? { inStock } : {})
};
products[index] = updated;
return updated;
}
async function remove(id) {
const index = products.findIndex(p => p.id === id);
if (index === -1) throw new NotFoundError('Product not found');
products.splice(index, 1);
}
module.exports = { list, getById, create, update, remove };
routes/products.js
// routes/products.js
const express = require('express');
const router = express.Router();
const store = require('../data/products-store');
const { BadRequestError } = require('../utils/errors');
// GET /products?inStock=true&minPrice=50&maxPrice=500
router.get('/', async (req, res, next) => {
try {
const { inStock, minPrice, maxPrice, limit, offset } = req.query;
const filters = {
inStock: inStock === undefined ? undefined : inStock === 'true',
minPrice: minPrice ? Number(minPrice) : undefined,
maxPrice: maxPrice ? Number(maxPrice) : undefined,
limit: limit ? Number(limit) : 20,
offset: offset ? Number(offset) : 0
};
const products = await store.list(filters);
res.json(products);
} catch (err) {
next(err);
}
});
// GET /products/:id
router.get('/:id', async (req, res, next) => {
try {
const id = Number(req.params.id);
const product = await store.getById(id);
res.json(product);
} catch (err) {
next(err);
}
});
// POST /products
router.post('/', async (req, res, next) => {
try {
const { name, price, inStock } = req.body;
if (!name || typeof price !== 'number') {
throw new BadRequestError('name and numeric price are required');
}
const product = await store.create({ name, price, inStock });
res.status(201).json(product);
} catch (err) {
next(err);
}
});
// PATCH /products/:id
router.patch('/:id', async (req, res, next) => {
try {
const id = Number(req.params.id);
const { name, price, inStock } = req.body;
const product = await store.update(id, { name, price, inStock });
res.json(product);
} catch (err) {
next(err);
}
});
// DELETE /products/:id
router.delete('/:id', async (req, res, next) => {
try {
const id = Number(req.params.id);
await store.remove(id);
res.status(204).send();
} catch (err) {
next(err);
}
});
module.exports = router;
app.js
// app.js
const express = require('express');
const productsRouter = require('./routes/products');
const { NotFoundError, BadRequestError } = require('./utils/errors');
const app = express();
app.use(express.json());
app.use('/products', productsRouter);
// 404 for unknown routes
app.use((req, res, next) => {
res.status(404).json({ error: 'route not found' });
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
if (err instanceof BadRequestError || err instanceof NotFoundError) {
return res.status(err.statusCode).json({ error: err.message });
}
res.status(500).json({ error: 'internal server error' });
});
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Products API listening on port ${PORT}`);
});
This third example of creating a RESTful API with Express.js shows how to keep your code organized as the API grows. Routes, data layer, and error utilities live in separate modules, which is the pattern most teams use once the codebase moves beyond a single file.
Beyond the 3 Examples: Patterns You’ll Reuse in 2024–2025
These 3 best examples of creating a RESTful API with Express.js aren’t just toy demos. They hide at least six reusable patterns you’ll carry into any Node.js backend:
- CRUD operations over resources (notes, users, products)
- Middleware for validation, authentication, and error handling
- JWT-based auth for stateless APIs
- Query-based filtering and pagination for listing endpoints
- Modular routing to keep code maintainable
- Data access abstraction so you can swap in real databases later
In recent years, many teams have started layering in TypeScript, OpenAPI/Swagger specs, and automated testing around these same patterns. If you want to document your Express APIs, check out the OpenAPI Initiative at openapis.org, which underpins many modern API documentation tools.
For general web development best practices and security basics that apply to Express.js APIs, the Mozilla Developer Network at developer.mozilla.org remains one of the most respected references worldwide.
FAQ: Real Questions About Express.js REST API Examples
What are some real examples of creating a RESTful API with Express.js?
Real-world examples of creating a RESTful API with Express.js include:
- A notes or tasks API for a productivity app
- A user accounts service handling registration, login, and profiles
- A products or inventory API for an online store
- An orders API that coordinates payments and shipping
- A notifications API that manages email or SMS events
The 3 examples in this guide (notes, users, products) are deliberately close to these real deployments.
Which example of Express.js REST API should I start with?
Start with the Notes API if you’re new to Express. Once you’re comfortable with routing and JSON, move to the User API to learn validation and JWT auth. Finally, use the Products API as a reference when you’re ready to structure a larger project with multiple files and a data layer.
Are these examples of creating a RESTful API with Express.js production-ready?
No. They’re intentionally trimmed down to highlight the patterns. In production, you’d add:
- Rate limiting and logging
- Real database integration
- Environment-based configuration
- Tests (unit and integration)
- Stronger security headers and input validation
The patterns, however, are the same ones used in many production Express apps.
Can I turn these 3 examples into a single Express.js monorepo?
Yes. Many teams organize services so that several examples of creating a RESTful API with Express.js live in the same codebase, each under its own route prefix or as separate microservices. You can share middleware (logging, auth, error handling) across them while keeping each domain (notes, users, products) isolated in its own folder.
Where can I learn more about HTTP and REST to improve these APIs?
To deepen your understanding of HTTP, status codes, and REST patterns, the HTTP documentation and web standards material on developer.mozilla.org is widely recommended by engineers and educators alike. Once you’re comfortable there, layering Express.js on top becomes much easier.
In short, these 3 best examples of creating a RESTful API with Express.js give you a clear path: start tiny, add auth and validation, then scale up your structure. From there, it’s just a matter of swapping out the in-memory arrays for a real database and wiring in the tooling your team prefers.
Related Topics
Real examples of deploying Node.js on Heroku: 3 practical examples
Practical examples of basic HTTP server examples in Node.js
3 Best Examples of Creating a RESTful API with Express.js
Examples of Multer File Uploads in Node.js: 3 Practical Patterns You’ll Actually Use
Modern examples of handling errors in Node.js applications
Modern examples of command-line application examples in Node.js
Explore More Node.js Code Snippets
Discover more examples and insights in this category.
View All Node.js Code Snippets