Building a REST API sounds straightforward until you actually try to do it right. I've seen too many Express.js applications that work fine in development but fall apart in production—missing error handling, CORS issues, unorganized routes, and middleware that's applied in the wrong order. When I first started with Express, I made all these mistakes, and I learned the hard way what actually matters.
Express.js has become the de facto standard for building REST APIs in Node.js, and for good reason. It's lightweight, flexible, and has a massive ecosystem. But that flexibility can be a double-edged sword—without proper structure, your API can quickly become a mess of unorganized routes and inconsistent error handling.
What is Express.js?
Express.js is a fast, unopinionated, minimalist web framework for Node.js. It provides:
- Routing - Simple, flexible routing system
- Middleware - Powerful middleware support
- Error handling - Built-in error handling mechanisms
- Template engines - Support for various template engines
- Static files - Serve static files easily
- REST API support - Perfect for building REST APIs
Installation
First, let's install Express.js and essential dependencies:
npm install express cors dotenv
npm install --save-dev nodemon
Basic Server Setup
Creating the main server file with proper middleware configuration:
const express = require("express");
const cors = require("cors");
const config = require("./config/env.config");
const database = require("./config/database");
const routes = require("./routes");
const app = express();
// Middleware
app.use(cors({
origin: config.cors.allowedOrigins,
credentials: true
}));
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ limit: "50mb", extended: true }));
app.use(express.static("uploads"));
// Routes
app.use("/", routes);
// 404 handler
app.use((req, res) => {
res.status(404).json({
success: false,
message: "Endpoint not found",
path: req.originalUrl,
});
});
// Error handler
app.use((err, req, res, next) => {
console.error("Error:", err);
if (err.name === "SequelizeValidationError") {
return res.status(400).json({
success: false,
message: "Validation error",
errors: err.errors.map((e) => ({
field: e.path,
message: e.message
})),
});
}
res.status(err.status || 500).json({
success: false,
message: err.message || "Internal server error",
});
});
// Async server startup
async function startServer() {
try {
const dbConnected = await database.testConnection();
if (!dbConnected) {
console.error("Failed to connect to database");
process.exit(1);
}
const PORT = config.server.port;
app.listen(PORT, () => {
console.log(`🚀 Server running on http://localhost:${PORT}`);
});
} catch (error) {
console.error("Error starting server:", error.message);
process.exit(1);
}
}
startServer();
Middleware Order Matters
The order of middleware is crucial:
- CORS - Must come first to handle preflight requests
- Body parsers - Parse request bodies before routes
- Static files - Serve static files
- Routes - Your API endpoints
- 404 handler - Catch undefined routes
- Error handler - Must be last to catch all errors
Best Practices
- Use proper middleware order
- Handle errors consistently
- Organize routes properly
- Use environment variables
- Implement proper CORS
- Add request logging
📖 Read the Complete Guide
This is just a brief overview! The complete guide on my blog includes:
- ✅ Environment Configuration - Proper config management
- ✅ Route Organization - Structuring routes and controllers
- ✅ Error Handling - Comprehensive error management
- ✅ Middleware Patterns - Custom middleware examples
- ✅ Database Integration - Connecting to databases
- ✅ Security Best Practices - Production security patterns
- ✅ Real-world examples from production applications
👉 Read the full article with all code examples here
What's your experience with Express.js? Share your tips in the comments! 🚀
For more backend guides, check out my blog covering JWT Authentication, Prisma ORM, Sequelize, and more.
Top comments (0)