Back to Blog
Node.js14 min read

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 nodemon

Basic 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.