DEV Community

Cover image for Express.js REST API Setup: Complete Guide for Production
Md. Maruf Rahman
Md. Maruf Rahman

Posted on • Originally published at marufrahman.live

Express.js REST API Setup: Complete Guide for Production

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.

📖 Want the complete guide with more examples and advanced patterns? Check out the full article on my blog for an in-depth tutorial with additional code examples, troubleshooting tips, and real-world use cases.

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
Enter fullscreen mode Exit fullscreen mode

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"));

// 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;
Enter fullscreen mode Exit fullscreen mode

Middleware Order Matters

The order of middleware is crucial:

  1. CORS - Must come first to handle preflight requests
  2. Body parsers - Parse request bodies before routes
  3. Static files - Serve static files
  4. Routes - Your API endpoints
  5. 404 handler - Catch undefined routes
  6. Error handler - Must be last to catch all errors

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";
  },
};
Enter fullscreen mode Exit fullscreen mode

Environment Variables

Create a .env file:

PORT=3000
API_VERSION=v1
NODE_ENV=development
FRONTEND_URL=http://localhost:5173,http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

CORS Configuration

CORS (Cross-Origin Resource Sharing) is essential for frontend integration:

const cors = require("cors");

// Basic CORS configuration
app.use(cors({
  origin: process.env.FRONTEND_URL || "http://localhost:5173",
  credentials: true,
  methods: ["GET", "POST", "PUT", "DELETE", "PATCH"],
  allowedHeaders: ["Content-Type", "Authorization"],
}));

// Advanced CORS with multiple origins
const allowedOrigins = [
  "http://localhost:5173",
  "http://localhost:3000",
  "https://yourdomain.com",
];

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
  credentials: true,
}));
Enter fullscreen mode Exit fullscreen mode

Organized Routing

Setting up route organization for maintainability:

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;
Enter fullscreen mode Exit fullscreen mode

Product Routes Example

Example of organized route file:

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;
Enter fullscreen mode Exit fullscreen mode

Error Handling

Proper error handling is crucial for production APIs:

// Error handling middleware (must be last)
app.use((err, req, res, next) => {
  console.error("Error:", err);

  // Sequelize validation errors
  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 
      })),
    });
  }

  // Sequelize unique constraint errors
  if (err.name === "SequelizeUniqueConstraintError") {
    return res.status(409).json({
      success: false,
      message: "Duplicate entry",
      field: err.errors[0]?.path,
    });
  }

  // JWT errors
  if (err.name === "JsonWebTokenError") {
    return res.status(401).json({
      success: false,
      message: "Invalid token",
    });
  }

  // Default error
  res.status(err.status || 500).json({
    success: false,
    message: err.message || "Internal server error",
    ...(process.env.NODE_ENV === "development" && { stack: err.stack }),
  });
});
Enter fullscreen mode Exit fullscreen mode

Custom Middleware

Creating custom middleware for logging, authentication, etc.:

// Request logging middleware
const requestLogger = (req, res, next) => {
  const start = Date.now();

  res.on("finish", () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.path} - ${res.statusCode} - ${duration}ms`);
  });

  next();
};

// Authentication middleware
const requireAuth = async (req, res, next) => {
  try {
    const token = req.headers.authorization?.replace("Bearer ", "");

    if (!token) {
      return res.status(401).json({
        success: false,
        message: "Authentication required",
      });
    }

    // Verify token and attach user to request
    const user = await verifyToken(token);
    req.user = user;
    next();
  } catch (error) {
    res.status(401).json({
      success: false,
      message: "Invalid token",
    });
  }
};

// Use middleware
app.use(requestLogger);
app.use("/api/v1/products", requireAuth);
Enter fullscreen mode Exit fullscreen mode

Request Validation

Validate incoming requests before processing:

const validateRequest = (schema) => {
  return (req, res, next) => {
    const { error } = schema.validate(req.body);

    if (error) {
      return res.status(400).json({
        success: false,
        message: "Validation error",
        errors: error.details.map((detail) => ({
          field: detail.path.join("."),
          message: detail.message,
        })),
      });
    }

    next();
  };
};

// Usage with Joi
const Joi = require("joi");

const productSchema = Joi.object({
  name: Joi.string().required(),
  price: Joi.number().positive().required(),
  stock: Joi.number().integer().min(0).required(),
});

router.post("/", validateRequest(productSchema), productController.create);
Enter fullscreen mode Exit fullscreen mode

API Response Format

Standardize your API responses:

// Success response helper
const sendSuccess = (res, data, message = "Success", statusCode = 200) => {
  res.status(statusCode).json({
    success: true,
    message,
    data,
  });
};

// Error response helper
const sendError = (res, message = "Error", statusCode = 500) => {
  res.status(statusCode).json({
    success: false,
    message,
  });
};

// Usage in controllers
class ProductController {
  async getAll(req, res) {
    try {
      const products = await productModel.getAll();
      sendSuccess(res, products, "Products retrieved successfully");
    } catch (error) {
      sendError(res, "Error retrieving products", 500);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Health Check Endpoint

Add a health check endpoint for monitoring:

router.get("/health", async (req, res) => {
  try {
    // Check database connection
    const dbStatus = await database.testConnection();

    res.json({
      success: true,
      status: "healthy",
      timestamp: new Date().toISOString(),
      database: dbStatus ? "connected" : "disconnected",
      uptime: process.uptime(),
    });
  } catch (error) {
    res.status(503).json({
      success: false,
      status: "unhealthy",
      error: error.message,
    });
  }
});
Enter fullscreen mode Exit fullscreen mode

Production Best Practices

  1. Use environment variables - Never hardcode configuration
  2. Implement proper error handling - Catch and handle all errors gracefully
  3. Configure CORS properly - Specify exact origins, not wildcards
  4. Organize routes by feature - Keep code maintainable as it grows
  5. Test database connection - Before starting the server
  6. Implement graceful shutdown - Close database connections properly
  7. Set body size limits - Prevent DoS attacks from large payloads
  8. Add request logging - For debugging and monitoring
  9. Use HTTPS in production - Encrypt all traffic
  10. Implement rate limiting - Prevent abuse

Rate Limiting Example

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
  message: "Too many requests from this IP, please try again later.",
});

app.use("/api/", limiter);
Enter fullscreen mode Exit fullscreen mode

Project Structure

Recommended project structure:

project/
├── config/
│   ├── database.js
│   └── env.config.js
├── controllers/
│   ├── productController.js
│   └── authController.js
├── middleware/
│   ├── auth.middleware.js
│   └── errorHandler.js
├── models/
│   └── index.js
├── routes/
│   ├── index.js
│   ├── product.routes.js
│   └── auth.routes.js
├── utils/
│   └── fileUpload.js
├── .env
├── app.js
└── server.js
Enter fullscreen mode Exit fullscreen mode

Resources and Further Reading

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.

Key Takeaways:

  • Express.js is the most popular Node.js framework for REST APIs
  • Middleware order matters - CORS first, body parsers before routes, error handler last
  • Organize routes by feature - Keep code maintainable as it grows
  • Proper error handling - Catch and handle all errors gracefully
  • CORS configuration - Essential for frontend integration
  • Environment variables - Never hardcode configuration
  • Health check endpoints - For monitoring and deployment
  • Graceful shutdown - Close connections properly

Whether you're building a simple CRUD API or a complex system with multiple features, this guide provides the foundation you need. Express.js makes API development simple, but following best practices ensures your API is production-ready and maintainable.


What's your experience with Express.js? Share your tips and tricks in the comments below! 🚀


💡 Looking for more details? This is a condensed version of my comprehensive guide. Read the full article on my blog for additional examples, advanced patterns, troubleshooting tips, and more in-depth explanations.

If you found this guide helpful, consider checking out my other articles on Node.js development and backend development best practices.

Top comments (0)