Create API endpoints for your frontend or mobile app. Learn routing, validation, authentication, and best practices.
A REST API lets your frontend (or mobile app) communicate with your backend through HTTP requests. It follows these conventions:
| Method | Purpose | Example |
|---|---|---|
| GET | Retrieve data | GET /api/users - List all users |
| POST | Create new data | POST /api/users - Create a user |
| PUT/PATCH | Update existing data | PUT /api/users/123 - Update user 123 |
| DELETE | Remove data | DELETE /api/users/123 - Delete user 123 |
Let's create a Node.js API using Express.js - the most popular choice.
# Initialize project
npm init -y
npm install express cors dotenv helmet
# Create folder structure
mkdir routes controllers middleware
touch app.js .env
// app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
require('dotenv').config();
const app = express();
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
// Health check
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
// Routes
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`API running on port ${PORT}`));
Organize your routes by resource (users, products, orders, etc.).
// routes/products.js
const express = require('express');
const router = express.Router();
const controller = require('../controllers/products');
router.get('/', controller.list); // GET /api/products
router.get('/:id', controller.getOne); // GET /api/products/:id
router.post('/', controller.create); // POST /api/products
router.put('/:id', controller.update); // PUT /api/products/:id
router.delete('/:id', controller.remove); // DELETE /api/products/:id
module.exports = router;
Implement the actual logic in your controllers.
// controllers/products.js
const { query } = require('../db');
exports.list = async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const { category, sort } = req.query;
let sql = 'SELECT * FROM products WHERE 1=1';
const params = [];
if (category) {
sql += ' AND category = ?';
params.push(category);
}
if (sort === 'price') sql += ' ORDER BY price ASC';
else if (sort === '-price') sql += ' ORDER BY price DESC';
else sql += ' ORDER BY created_at DESC';
sql += ' LIMIT ? OFFSET ?';
params.push(limit, offset);
const products = await query(sql, params);
const [{ total }] = await query('SELECT COUNT(*) as total FROM products');
res.json({
data: products,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit)
}
});
};
exports.getOne = async (req, res) => {
const products = await query('SELECT * FROM products WHERE id = ?', [req.params.id]);
if (products.length === 0) {
return res.status(404).json({ error: 'Product not found' });
}
res.json(products[0]);
};
exports.create = async (req, res) => {
const { name, price, category, description } = req.body;
if (!name || !price || !category) {
return res.status(400).json({ error: 'Name, price, and category required' });
}
const result = await query(
'INSERT INTO products (name, price, category, description) VALUES (?, ?, ?, ?)',
[name, price, category, description || null]
);
res.status(201).json({
id: result.insertId,
name, price, category, description
});
};
exports.update = async (req, res) => {
const { name, price, category, description } = req.body;
const { id } = req.params;
const result = await query(
'UPDATE products SET name = ?, price = ?, category = ?, description = ? WHERE id = ?',
[name, price, category, description, id]
);
if (result.affectedRows === 0) {
return res.status(404).json({ error: 'Product not found' });
}
res.json({ id, name, price, category, description });
};
exports.remove = async (req, res) => {
const result = await query('DELETE FROM products WHERE id = ?', [req.params.id]);
if (result.affectedRows === 0) {
return res.status(404).json({ error: 'Product not found' });
}
res.status(204).send();
};
Always validate input data before processing it.
const Joi = require('joi');
const productSchema = Joi.object({
name: Joi.string().min(3).max(100).required(),
price: Joi.number().positive().required(),
category: Joi.string().valid('electronics', 'clothing', 'books', 'other').required(),
description: Joi.string().max(1000).optional()
});
// Validation middleware
function validate(schema) {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(d => d.message)
});
}
next();
};
}
// Apply to routes
router.post('/', validate(productSchema), controller.create);
router.put('/:id', validate(productSchema), controller.update);
Protect your API endpoints from unauthorized access.
const jwt = require('jsonwebtoken');
// Login endpoint
router.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await validateCredentials(email, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token });
});
// Auth middleware
function authMiddleware(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Invalid token' });
}
}
// Protect routes
router.post('/', authMiddleware, controller.create);
router.put('/:id', authMiddleware, controller.update);
router.delete('/:id', authMiddleware, controller.remove);
Handle errors consistently across your API.
// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Endpoint not found' });
});
// Global error handler (must be last)
app.use((error, req, res, next) => {
console.error(`[${new Date().toISOString()}]`, error);
const statusCode = error.statusCode || 500;
const message = error.message || 'Internal server error';
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV !== 'production' && { stack: error.stack })
});
});
// Wrap async handlers to catch errors
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Use in routes
router.get('/', asyncHandler(controller.list));
Document your API so others (and future you) can use it.
Use Swagger/OpenAPI for interactive documentation. It auto-generates a UI where developers can test your endpoints.
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = swaggerJsdoc({
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
},
servers: [{ url: '/api' }],
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer' }
}
}
},
apis: ['./routes/*.js']
});
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));