# Express.js Complete Tutorial: Build Production-Ready REST APIs

# Express.js Complete Tutorial: Build Production-Ready REST APIs

Express.js has become the de facto standard for building web applications and REST APIs in the Node.js ecosystem. Its minimalist yet powerful approach makes it an ideal choice for developers looking to create scalable, production-ready applications. In this comprehensive tutorial, we'll explore how to build a robust REST API from the ground up.

## Getting Started with Express.js

Express.js is a fast, unopinionated web framework for Node.js that provides a thin layer of fundamental web application features. Before diving in, ensure you have Node.js installed on your system.

First, initialize a new project and install the necessary dependencies:

```bash
mkdir express-api
cd express-api
npm init -y
npm install express dotenv cors helmet morgan
npm install --save-dev nodemon
```

These packages include Express itself, environment variable management (dotenv), CORS handling, security middleware (helmet), and HTTP request logging (morgan).

## Project Structure

A well-organized project structure is crucial for maintainability:

```
express-api/
├── src/
│   ├── config/
│   ├── controllers/
│   ├── middleware/
│   ├── models/
│   ├── routes/
│   └── utils/
├── .env
├── .gitignore
└── server.js
```

## Building the Foundation

Create your main `server.js` file:

```javascript
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // Enable CORS
app.use(morgan('combined')); // Logging
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
```

## Creating RESTful Routes

Let's build a complete CRUD API for a "users" resource. Create `src/routes/users.js`:

```javascript
const express = require('express');
const router = express.Router();
const userController = require('../controllers/userController');
const { validateUser } = require('../middleware/validation');

router.get('/', userController.getAllUsers);
router.get('/:id', userController.getUserById);
router.post('/', validateUser, userController.createUser);
router.put('/:id', validateUser, userController.updateUser);
router.delete('/:id', userController.deleteUser);

module.exports = router;
```

## Implementing Controllers

Controllers handle the business logic. Create `src/controllers/userController.js`:

```javascript
// In-memory storage (replace with database in production)
let users = [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];

exports.getAllUsers = (req, res) => {
  try {
    const { page = 1, limit = 10 } = req.query;
    const startIndex = (page - 1) * limit;
    const endIndex = page * limit;
    
    const paginatedUsers = users.slice(startIndex, endIndex);
    
    res.status(200).json({
      success: true,
      count: paginatedUsers.length,
      total: users.length,
      data: paginatedUsers
    });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
};

exports.getUserById = (req, res) => {
  try {
    const user = users.find(u => u.id === parseInt(req.params.id));
    
    if (!user) {
      return res.status(404).json({ 
        success: false, 
        error: 'User not found' 
      });
    }
    
    res.status(200).json({ success: true, data: user });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
};

exports.createUser = (req, res) => {
  try {
    const newUser = {
      id: users.length + 1,
      name: req.body.name,
      email: req.body.email
    };
    
    users.push(newUser);
    
    res.status(201).json({ success: true, data: newUser });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
};

exports.updateUser = (req, res) => {
  try {
    const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
    
    if (userIndex === -1) {
      return res.status(404).json({ 
        success: false, 
        error: 'User not found' 
      });
    }
    
    users[userIndex] = { 
      ...users[userIndex], 
      ...req.body 
    };
    
    res.status(200).json({ success: true, data: users[userIndex] });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
};

exports.deleteUser = (req, res) => {
  try {
    const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));
    
    if (userIndex === -1) {
      return res.status(404).json({ 
        success: false, 
        error: 'User not found' 
      });
    }
    
    users.splice(userIndex, 1);
    
    res.status(200).json({ success: true, data: {} });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
};
```

## Validation Middleware

Input validation is critical for production APIs. Create `src/middleware/validation.js`:

```javascript
exports.validateUser = (req, res, next) => {
  const { name, email } = req.body;
  
  if (!name || name.trim().length === 0) {
    return res.status(400).json({ 
      success: false, 
      error: 'Name is required' 
    });
  }
  
  if (!email || !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
    return res.status(400).json({ 
      success: false, 
      error: 'Valid email is required' 
    });
  }
  
  next();
};
```

## Error Handling Middleware

Create `src/middleware/errorHandler.js`:

```javascript
exports.errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  
  res.status(err.statusCode || 500).json({
    success: false,
    error: err.message || 'Server Error',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  });
};

exports.notFound = (req, res, next) => {
  res.status(404).json({
    success: false,
    error: 'Route not found'
  });
};
```

## Integrating Everything

Update your `server.js` to include routes and error handling:

```javascript
// ... previous imports
const userRoutes = require('./src/routes/users');
const { errorHandler, notFound } = require('./src/middleware/errorHandler');

// ... middleware setup

// Routes
app.use('/api/v1/users', userRoutes);

// Error handling
app.use(notFound);
app.use(errorHandler);

// ... server listen
```

## Production Considerations

For production deployment, consider these enhancements:

1. **Database Integration**: Replace in-memory storage with MongoDB, PostgreSQL, or another database
2. **Authentication**: Implement JWT-based authentication
3. **Rate Limiting**: Use `express-rate-limit` to prevent abuse
4. **Compression**: Add `compression` middleware for response optimization
5. **Environment Variables**: Store sensitive data in `.env` files
6. **Logging**: Implement Winston or Bunyan for advanced logging
7. **Testing**: Add Jest or Mocha for unit and integration tests
8. **Documentation**: Use Swagger/OpenAPI for API documentation

## Conclusion

This tutorial covered the essentials of building production-ready REST APIs with Express.js. You've learned about project structure, routing, controllers, middleware, validation, and error handling. The modular architecture presented here scales well and follows industry best practices. As you continue developing, focus on security, performance optimization, and comprehensive testing to ensure your API is truly production-ready.
