Node.js and Express.js: Building RESTful APIs and Web Applications
Introduction
Node.js has revolutionized backend development by enabling JavaScript to run on the server, creating a unified language ecosystem for full-stack development. Express.js, built on top of Node.js, has become the de facto standard web framework for building APIs and web applications.
This comprehensive guide covers Node.js and Express.js from fundamentals to advanced topics. You'll learn how to build RESTful APIs, handle asynchronous operations, implement middleware, manage authentication, and deploy production-ready applications.
What is Node.js?
Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine. It allows JavaScript to run outside the browser, enabling server-side development with JavaScript.
Key Features:
- Event-Driven: Non-blocking I/O operations
- Asynchronous: Handles concurrent operations efficiently
- Single-Threaded: Uses event loop for concurrency
- NPM: Largest package ecosystem
- Cross-Platform: Runs on Windows, macOS, Linux
Why Node.js?
- JavaScript Everywhere: Same language for frontend and backend
- High Performance: Efficient for I/O-intensive applications
- Large Ecosystem: Millions of packages available
- Active Community: Strong community support
- Real-time Applications: Excellent for WebSockets and real-time features
Getting Started with Node.js
Setting up Node.js is straightforward:
# Check Node.js version
node --version
# Check npm version
npm --version
# Install Node.js from nodejs.org
# Or use nvm (Node Version Manager)
nvm install 18
nvm use 18
// app.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, Node.js!');
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
node app.js
// package.json
{
"type": "module"
}
// app.js
import http from 'http';
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, Node.js!');
});
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
Asynchronous Programming in Node.js
Understanding asynchronous programming is crucial for Node.js:
// Callback example
const fs = require('fs');
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('File content:', data);
});
// Promise example
const fs = require('fs').promises;
fs.readFile('file.txt', 'utf8')
.then(data => {
console.log('File content:', data);
})
.catch(err => {
console.error('Error:', err);
});
// Async/await example
const fs = require('fs').promises;
async function readFile() {
try {
const data = await fs.readFile('file.txt', 'utf8');
console.log('File content:', data);
} catch (err) {
console.error('Error:', err);
}
}
readFile();
// Understanding the event loop
console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
console.log('4');
// Output: 1, 4, 3, 2
Introduction to Express.js
Express.js is a minimal and flexible Node.js web application framework:
Key Features:
- Define routes easily
- Extend functionality
- Support for various templating
- Serve static assets
- Built-in error handling
Installing Express:
npm init -y
npm install express
// app.js
const express = require('express');
const app = express();
const PORT = 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Hello, Express!' });
});
app.get('/api/users', (req, res) => {
res.json([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Express.js Routing
Express provides powerful routing capabilities:
// GET request
app.get('/users', (req, res) => {
res.json({ users: [] });
});
// POST request
app.post('/users', (req, res) => {
const user = req.body;
// Create user
res.status(201).json({ user });
});
// PUT request
app.put('/users/:id', (req, res) => {
const { id } = req.params;
const user = req.body;
// Update user
res.json({ user });
});
// DELETE request
app.delete('/users/:id', (req, res) => {
const { id } = req.params;
// Delete user
res.status(204).send();
});
// Route with parameters
app.get('/users/:id', (req, res) => {
const { id } = req.params;
res.json({ id, name: 'John' });
});
// Multiple parameters
app.get('/users/:userId/posts/:postId', (req, res) => {
const { userId, postId } = req.params;
res.json({ userId, postId });
});
// Query parameters
app.get('/users', (req, res) => {
const { page, limit, sort } = req.query;
res.json({ page, limit, sort });
});
// Example: /users?page=1&limit=10&sort=name
// Multiple handlers
app.get('/users',
(req, res, next) => {
console.log('Middleware 1');
next();
},
(req, res, next) => {
console.log('Middleware 2');
next();
},
(req, res) => {
res.json({ users: [] });
}
);
Express Middleware
Middleware functions have access to request, response, and next:
// Logger middleware
const logger = (req, res, next) => {
console.log(`${req.method} ${req.path} - ${new Date().toISOString()}`);
next();
};
app.use(logger);
// Authentication middleware
const authenticate = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
// Verify token
req.user = { id: 1, name: 'John' };
next();
};
app.use('/api/protected', authenticate);
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({
error: 'Something went wrong!',
message: err.message
});
});
// Using error middleware
app.get('/error', (req, res, next) => {
try {
throw new Error('Something went wrong');
} catch (err) {
next(err);
}
});
// CORS
const cors = require('cors');
app.use(cors());
// Helmet (security)
const helmet = require('helmet');
app.use(helmet());
// Rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
Building RESTful APIs
Express makes building RESTful APIs straightforward:
// routes/users.js
const express = require('express');
const router = express.Router();
let users = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' }
];
// GET /users
router.get('/', (req, res) => {
res.json(users);
});
// GET /users/:id
router.get('/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// POST /users
router.post('/', (req, res) => {
const { name, email } = req.body;
const newUser = {
id: users.length + 1,
name,
email
};
users.push(newUser);
res.status(201).json(newUser);
});
// PUT /users/:id
router.put('/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
Object.assign(user, req.body);
res.json(user);
});
// DELETE /users/:id
router.delete('/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
module.exports = router;
// app.js
const express = require('express');
const usersRouter = require('./routes/users');
const app = express();
app.use(express.json());
app.use('/api/users', usersRouter);
Database Integration
Integrate databases with Express applications:
// models/User.js
const mongoose = require('mongoose');
const userSchema = new mongoose.Schema({
name: { type: String, required: true },
email: { type: String, required: true, unique: true },
createdAt: { type: Date, default: Date.now }
});
module.exports = mongoose.model('User', userSchema);
// Connect to MongoDB
mongoose.connect('mongodb://localhost:27017/myapp', {
useNewUrlParser: true,
useUnifiedTopology: true
});
// routes/users.js
const User = require('../models/User');
router.get('/', async (req, res) => {
try {
const users = await User.find();
res.json(users);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.post('/', async (req, res) => {
try {
const user = new User(req.body);
await user.save();
res.status(201).json(user);
} catch (err) {
res.status(400).json({ error: err.message });
}
});
// db.js
const { Pool } = require('pg');
const pool = new Pool({
user: 'user',
host: 'localhost',
database: 'myapp',
password: 'password',
port: 5432
});
module.exports = pool;
// routes/users.js
const pool = require('../db');
router.get('/', async (req, res) => {
try {
const result = await pool.query('SELECT * FROM users');
res.json(result.rows);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
Authentication and Authorization
Implement authentication in Express applications:
// middleware/auth.js
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
module.exports = authenticate;
// routes/auth.js
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
router.post('/register', async (req, res) => {
try {
const { name, email, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = new User({ name, email, password: hashedPassword });
await user.save();
res.status(201).json({ message: 'User created' });
} catch (err) {
res.status(400).json({ error: err.message });
}
});
router.post('/login', async (req, res) => {
try {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const token = jwt.sign(
{ userId: user._id },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({ token });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
Error Handling and Validation
Proper error handling and validation are essential:
// middleware/validate.js
const { body, validationResult } = require('express-validator');
const validateUser = [
body('name').trim().isLength({ min: 1 }).withMessage('Name is required'),
body('email').isEmail().withMessage('Valid email is required'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
];
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
};
router.post('/register', validateUser, handleValidationErrors, async (req, res) => {
// Registration logic
});
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
if (err.name === 'ValidationError') {
return res.status(400).json({ error: err.message });
}
if (err.name === 'UnauthorizedError') {
return res.status(401).json({ error: 'Unauthorized' });
}
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
};
app.use(errorHandler);
Deployment and Production Best Practices
Deploy Express applications to production:
// .env
NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
JWT_SECRET=your-secret-key
// app.js
require('dotenv').config();
const PORT = process.env.PORT || 3000;
// Security middleware
const helmet = require('helmet');
app.use(helmet());
// Compression
const compression = require('compression');
app.use(compression());
// Rate limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', limiter);
# Using PM2
npm install -g pm2
pm2 start app.js
pm2 save
pm2 startup
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "app.js"]
Conclusion
Node.js and Express.js provide a powerful platform for building modern web applications and RESTful APIs. By understanding asynchronous programming, routing, middleware, and best practices, you can build scalable, maintainable applications.
Start with simple applications and gradually add more features. Focus on proper error handling, security, and code organization to build production-ready applications. Remember that Node.js excels at I/O-intensive applications, making it ideal for APIs, real-time applications, and microservices.
With the right approach, Node.js and Express.js can help you build fast, scalable applications that leverage JavaScript's ecosystem and Node.js's event-driven architecture.