Express.js REST API Setup – Complete Guide
Learn how to set up a production-ready Express.js REST API with CORS, error handling, middleware, and routing. Complete guide with code examples for building REST APIs.
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.
In this guide, I'll walk you through setting up an Express.js REST API the way I do it in production. We'll cover middleware organization (the order matters more than you might think), proper error handling (so your API doesn't crash when something goes wrong), CORS configuration (essential for frontend integration), and route organization (so your code stays maintainable as it grows). I'll also share some gotchas I've encountered that aren't always obvious from the documentation.
Installation
npm install express cors dotenv
npm install --save-dev nodemonBasic Server Setup
Creating the main server file:
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"));
// Development logging
if (config.isDevelopment()) {
app.use((req, res, next) => {
console.log(`${req.method} ${req.path}`);
next();
});
}
// 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}`);
console.log(`📊 Database: MySQL (Sequelize ORM)`);
console.log(`🔗 API: /api/${config.server.apiVersion}`);
});
} catch (error) {
console.error("Error starting server:", error.message);
process.exit(1);
}
}
startServer();
// Graceful shutdown
process.on("SIGINT", async () => {
console.log("Shutting down...");
await database.closeConnection();
process.exit(0);
});
module.exports = app;Environment Configuration
Setting up environment configuration:
require("dotenv").config();
module.exports = {
server: {
port: process.env.PORT || 3000,
apiVersion: process.env.API_VERSION || "v1",
nodeEnv: process.env.NODE_ENV || "development",
},
cors: {
allowedOrigins: process.env.FRONTEND_URL
? process.env.FRONTEND_URL.split(",")
: ["http://localhost:5173"],
},
isDevelopment: () => {
return process.env.NODE_ENV === "development";
},
};Organized Routing
Setting up route organization:
const express = require("express");
const router = express.Router();
const config = require("../config/env.config");
const authRoutes = require("./auth.routes");
const productRoutes = require("./product.routes");
const categoryRoutes = require("./category.routes");
// API version prefix
const apiPrefix = `/api/${config.server.apiVersion}`;
// Health check
router.get("/health", (req, res) => {
res.json({
success: true,
message: "API is running",
timestamp: new Date().toISOString(),
});
});
// Routes
router.use(`${apiPrefix}/auth`, authRoutes);
router.use(`${apiPrefix}/products`, productRoutes);
router.use(`${apiPrefix}/categories`, categoryRoutes);
module.exports = router;Product Routes Example
const express = require("express");
const router = express.Router();
const productController = require("../controllers/productController");
const { verifyToken } = require("../middleware/auth.middleware");
const { storeProductFiles } = require("../utils/fileUpload");
// All routes require authentication
router.use(verifyToken);
// GET routes
router.get("/", productController.getAll);
router.get("/:id", productController.getById);
router.get("/category/:categoryId", productController.getByCategoryId);
// POST route with file upload
router.post("/", async (req, res, next) => {
await storeProductFiles(req, res, next);
productController.create(req, res);
});
// PUT route with file upload
router.put("/:id", async (req, res, next) => {
await storeProductFiles(req, res, next);
productController.update(req, res);
});
// DELETE route
router.delete("/:id", productController.delete);
module.exports = router;Best Practices
- Always use environment variables for configuration
- Implement proper error handling middleware
- Use CORS configuration for cross-origin requests
- Organize routes by feature/module
- Test database connection before starting server
- Implement graceful shutdown for database connections
- Use body size limits for file uploads
- Add request logging in development mode
Conclusion
Express.js provides a robust foundation for building REST APIs. With proper middleware configuration, error handling, and route organization, you can create scalable, maintainable APIs. This setup is production-ready and follows best practices for inventory management systems and other backend applications.
Related Articles
JWT Authentication in Express.js and Node.js: Complete Guide
Learn how to implement JWT authentication with bcrypt password hashing and protected routes.
Sequelize ORM with MySQL Setup: Complete Guide for Node.js
Complete guide with connection pooling, migrations, and best practices for database setup.
Multer File Upload in Express.js: Complete Guide with Examples
Learn how to implement file uploads in Express.js using Multer with validation.
Cloudinary Image Upload in Node.js: Complete Guide with Examples
Implement Cloudinary image upload with optimization and transformations.