Building a REST API with Node.js and Express
Introduction
Building a REST API is a fundamental skill for backend developers. In this tutorial, we'll create a complete REST API using Node.js and Express, covering CRUD operations, middleware, and error handling.
Project Setup
First, initialize a new Node.js project:
mkdir my-api
cd my-api
npm init -y
npm install express
npm install --save-dev nodemon
Basic Server Setup
Create server.js:
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(express.json());
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Welcome to my API' });
});
// Start server
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Creating Routes
Let's create a simple blog API with posts:
// In-memory database (for demo purposes)
let posts = [
{ id: 1, title: 'First Post', content: 'Hello World' },
{ id: 2, title: 'Second Post', content: 'Learning Node.js' }
];
// GET all posts
app.get('/api/posts', (req, res) => {
res.json(posts);
});
// GET single post
app.get('/api/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 post
app.post('/api/posts', (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ error: 'Title and content required' });
}
const newPost = {
id: posts.length + 1,
title,
content
};
posts.push(newPost);
res.status(201).json(newPost);
});
// UPDATE post
app.put('/api/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' });
}
const { title, content } = req.body;
if (title) post.title = title;
if (content) post.content = content;
res.json(post);
});
// DELETE post
app.delete('/api/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' });
}
posts.splice(index, 1);
res.status(204).send();
});
Adding Middleware
Create custom middleware for logging and error handling:
// Logger middleware
const logger = (req, res, next) => {
console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
next();
};
// Error handler middleware
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
};
// Use middleware
app.use(logger);
app.use(errorHandler);
Input Validation
Add validation using express-validator:
const { body, validationResult } = require('express-validator');
app.post('/api/posts',
[
body('title')
.trim()
.isLength({ min: 3, max: 100 })
.withMessage('Title must be 3-100 characters'),
body('content')
.trim()
.isLength({ min: 10 })
.withMessage('Content must be at least 10 characters')
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Create post...
}
);
Organizing Routes
For larger applications, organize routes into separate files:
// routes/posts.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
// Get all posts
});
router.post('/', (req, res) => {
// Create post
});
module.exports = router;
// server.js
const postsRouter = require('./routes/posts');
app.use('/api/posts', postsRouter);
Database Integration
Connect to MongoDB using Mongoose:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/my-api', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// Define schema
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
minlength: 3,
maxlength: 100
},
content: {
type: String,
required: true,
minlength: 10
},
createdAt: {
type: Date,
default: Date.now
}
});
const Post = mongoose.model('Post', postSchema);
// Use in routes
app.post('/api/posts', async (req, res) => {
try {
const post = new Post({
title: req.body.title,
content: req.body.content
});
await post.save();
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Testing the API
Test your endpoints using curl:
# Get all posts
curl http://localhost:3000/api/posts
# Create a post
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title":"New Post","content":"This is a new post"}'
# Update a post
curl -X PUT http://localhost:3000/api/posts/1 \
-H "Content-Type: application/json" \
-d '{"title":"Updated Title"}'
# Delete a post
curl -X DELETE http://localhost:3000/api/posts/1
Best Practices
- Use environment variables for configuration
- Implement proper error handling at all levels
- Validate input data before processing
- Use async/await for asynchronous operations
- Add rate limiting to prevent abuse
- Implement authentication for protected routes
- Use HTTPS in production
- Add request logging for debugging
Conclusion
You've now built a complete REST API with Express! This foundation can be extended with authentication, real databases, caching, and many other features.
Next Steps
- Add JWT authentication
- Implement pagination for list endpoints
- Add search and filtering
- Set up automated testing
- Deploy to a cloud platform