🌐Building APIs with Express

Build RESTful APIs using Express, the most widely used framework in the Node ecosystem.

🏗 Setting Up Express Projects

Welcome to setting up Express projects! In this chapter, you'll learn how to create and organize Express applications effectively. We'll cover the essentials of project structure, best practices, and tools that will help you build robust APIs.

💡 Key Features of Express Projects

  • Modular structure for better code organization
  • §Middleware support° for handling HTTP requests and responses
  • ⌘Route management° for defining API endpoints
  • ¶Environment configuration° for development and production settings
  • §File organization best practices°

Setting Up a New Express Project

Let's start by creating a new project using the Node.js command line. You can use either npm or yarn to initialize your project.

mkdir my-express-app
 cd my-express-app
 npm init -y

Install Express and required dependencies:

npm install express dotenv --save
 npm install nodemon --save-dev

💡 Best Practices for File/Folder Organization

  • Create a /src folder as your main project directory
  • Organize routes in separate files within a /routes folder
  • Store middleware functions in a /middleware folder
  • Put configuration files in a /config folder
  • Use a /public folder for static assets

Common Mistakes to Avoid

  • Don't mix development and production code in the same environment
  • Don't place sensitive data in the main configuration file
  • Don't ignore error handling middleware
  • Avoid nesting routes deeply without proper organization
  • Don't use default port 3000 for production applications

Using Boilerplates and Templates

Boilerplates can save you time when starting a new project. Look for popular Express boilerplates on GitHub or npm.

// Example: Using an Express boilerplate
 git clone https://github.com/example/express-boilerplate.git my-project

Example of a Basic Express Setup

// src/app.js
 const express = require('express');
 
 const app = express();
 
 // Middleware
 app.use(express.json());
 app.use(express.urlencoded({ extended: true }));
 
 // Routes
 app.use('/api', require('./routes/api'));

This setup creates a modular project with separate routes and middleware. As your application grows, you can easily add new features without disrupting existing functionality.

💡 Real-World Project Structure Example

// Folder structure
 /my-express-app
 ├── src/
 │   ├── app.js
 │   ├── routes/
 │   ├── middleware/
 │   └── config/
 ├── package.json
 ├── .env
 └── README.md

💡 Summary

By following these best practices, you'll create maintainable and scalable Express projects. Proper file organization, using middleware effectively, and adhering to security guidelines will ensure your API is robust and performant.

🧭 Middleware & Routing

Welcome to the chapter on Middleware & Routing in Node.js! In this section, you'll learn about custom middleware, organizing routes with `express.Router`, and how to structure your API endpoints effectively.

💡 What is Middleware?

Middleware functions are used in Express.js to perform tasks such as: authenticating requests, logging information, managing errors, and processing the request before it reaches your routes.

const express = require('express');
const app = express();

// Middleware to log requests
app.use((req, res, next) => {
  console.log(`Incoming request: ${req.method} ${req.url}`);
  next();
});

Creating Custom Middleware

Custom middleware can be created to handle specific tasks. It's important to remember to call `next()` when passing control to the next middleware in the stack.

// Synchronous middleware
function checkAuth(req, res, next) {
  if (req.headers.authorization === 'Bearer valid-token') {
    next();
  } else {
    res.status(401).send('Unauthorized');
  }
}

// Asynchronous middleware with Promises
async function logRequest(req, res, next) {
  try {
    await someAsyncOperation();
    console.log(`Logged request: ${req.url}`);
    next();
  } catch (error) {
    res.status(500).send('Internal Server Error');
  }
}

Organizing Routes with express.Router

The `express.Router()` object is used to create modular route handlers. This makes your code more organized and easier to maintain.

// Routes for /api/users
const router = express.Router();

router.get('/', (req, res) => {
  res.send('Get all users');
});

router.post('/', (req, res) => {
  res.send('Create a new user');
});

// Mount the router on /api/users
app.use('/api/users', router);

💡 Route-Level Middleware

Middleware can be added at a route level to handle specific functionality for that endpoint.

app.get('/api/data', checkAuth, (req, res) => {
  res.send('Protected data');
});

Best Practices for Middleware

  • Keep middleware functions small and focused.
  • Use `express.Router` to organize routes by functionality.
  • Always handle errors appropriately in middleware.
  • Avoid using too many layers of middleware, which can slow down your app.

💡 Real-World Applications

Middleware is essential for creating robust APIs. For example, you might use middleware to: validate tokens, compress responses, or handle CORS requests.

🧼 Data Validation & Sanitization

When building APIs with Express, ensuring data integrity is critical for security, reliability, and maintaining trust with your users. Data validation involves checking that incoming requests meet specific criteria before processing them. Sanitization ensures that any user input is cleaned or modified to prevent malicious attacks like SQL injection or XSS vulnerabilities.

💡 Why Validate and Sanitize Data?

  • Prevent invalid data from entering your system
  • Protect against malicious attacks
  • Ensure consistent data quality
  • Provide clear error messages to users
  • Maintain the integrity of your application's logic

💡 Introduction to Joi

Joi is a popular library for schema validation. It provides a powerful, flexible way to define and validate data structures.

npm install joi

const Joi = require('joi');

// Create a schema
const userSchema = Joi.object({
  name: Joi.string().min(3).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(18)
});

Validating a Request

app.post('/users', (req, res) => {
  const { error } = userSchema.validate(req.body);

  if (error) {
    return res.status(400).json({
      message: 'Validation failed',
      details: error.details
    });
  }

  // Proceed with creating the user
});

💡 Introduction to zod

Zod is a TypeScript-first schema declaration and validation library. It provides an alternative syntax that can be more concise and integrates well with TypeScript.

npm install zod

import { z } from 'zod';

// Create a schema
const userSchema = z.object({
  name: z.string().min(3),
  email: z.string().email(),
  age: z.number().int().min(18)
});

Validating with zod

app.post('/users', (req, res) => {
  try {
    const validatedData = userSchema.parse(req.body);

    // Proceed with creating the user using validatedData
  } catch (error) {
    if (error instanceof z.ZodError) {
      return res.status(400).json({
        message: 'Validation failed',
        details: error.issues
      });
    }
  }
});

💡 Introduction to express-validator

Express-validator is a middleware library that provides validation utilities for Express routes. It integrates seamlessly with Express and supports both synchronous and asynchronous validation.

npm install express-validator

const { check, validationResult } = require('express-validator');

app.post('/users', [
  check('name').not().isEmpty().withMessage('Name is required')
          .isLength({ min: 3 }).withMessage('Name must be at least 3 characters long'),
  check('email').not().isEmpty().withMessage('Email is required')
          .isEmail().withMessage('Invalid email format'),
  check('age').not().isEmpty().withMessage('Age is required')
          .isInt({ min: 18 }).withMessage('Must be at least 18 years old')
], (req, res) => {
  const errors = validationResult(req);

  if (!errors.isEmpty()) {
    return res.status(400).json({
      message: 'Validation failed',
      details: errors.array()
    });
  }

  // Proceed with creating the user
});

💡 Comparing Joi, zod, and express-validator

  • Joi is great for complex validation schemas and has a large community.
  • zod provides TypeScript-first validation with a more modern syntax.
  • express-validator integrates well with Express routes and provides middleware-based validation.

💡 Sanitizing Data

Sanitization ensures that any user input is safe to use. This includes removing or modifying unsafe characters, encoding special characters, and ensuring data is in a usable format.

app.use(express.urlencoded({ extended: true }));

// Use express-validator's sanitize methods
app.post('/users', [
  check('name').trim().escape(),
  check('email').normalizeEmail(),
  check('bio').stripLowSpecialChars()
], (req, res) => {
  // Process sanitized data
});

💡 Best Practices for Data Validation and Sanitization

  • Always validate and sanitize all user input, even if it comes from trusted sources.
  • Use centralized validation schemas to maintain consistency across your application.
  • Provide meaningful error messages that help users understand what went wrong.
  • Sanitize data early in the request lifecycle to prevent any unsafe data from being processed further.
  • Test your validation and sanitization logic thoroughly to ensure it catches all edge cases.

🔒 Authentication & Authorization

Welcome to the world of authentication and authorization in Express! Today, we'll explore how to implement secure authentication mechanisms and control access to your API endpoints. We'll cover sessions, JWT (JSON Web Tokens), OAuth2, and role-based access control (RBAC).

💡 What is Authentication?

Authentication is the process of verifying the identity of a user or client. It answers the question: Who are you? In Express, we can implement authentication using several methods.

  • Sessions: Storing user information on the server
  • JWT: Token-based authentication
  • OAuth2: Delegate authentication to a third party

💡 What is Authorization?

Authorization is the process of determining what resources a user or client is allowed to access. It answers the question: What are you allowed to do? We'll explore how to implement role-based access control (RBAC).

Implementing Sessions in Express

Sessions are a common method of authentication where user data is stored on the server and accessed via a session ID.

const express = require('express');
const session = require('express-session');

const app = express();

app.use(session({
  secret: 'your-secret-key',
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: true,
    httpOnly: true,
    sameSite: 'lax'
  }
}));

💡 Best Practices for Session Security

  • Always use HTTPS to encrypt session cookies
  • Set the HttpOnly, Secure, and SameSite flags on cookies
  • Regenerate session IDs after login
  • Implement session timeout

JWT Authentication in Express

JSON Web Tokens (JWT) are a popular method for token-based authentication. They allow users to authenticate without maintaining sessions on the server.

const express = require('express');
const jwt = require('jsonwebtoken');

app.post('/login', async (req, res) => {
  const user = await User.findOne({ email: req.body.email });
  
  if (!user || !await user.comparePassword(req.body.password)) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, {
    expiresIn: 86400 // 24 hours
  });

  res.json({ token });
});

OAuth2 with Express

OAuth2 is a delegate authentication framework that allows users to authenticate using third-party accounts (e.g., Google, GitHub).

const express = require('express');
const passport = require('passport');
const GitHubStrategy = require('passport-github2').Strategy;

passport.use(new GitHubStrategy({
  clientID: process.env.GITHUB_CLIENT_ID,
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
}, (accessToken, refreshToken, profile, done) => {
  User.findOrCreate({ githubId: profile.id }, (err, user) => {
    done(err, user);
  });
}));

Role-Based Access Control in Express

RBAC allows users to access resources based on their roles and permissions. We'll implement this using middleware.

const express = require('express');

app.use((req, res, next) => {
  const userRoles = req.user.roles || [];
  
  if (!userRoles.includes('admin') && req.path.startsWith('/admin')) {
    return res.status(403).json({ message: 'Forbidden' });
  }

  next();
});

Common Mistakes to Avoid

  • Don't store sensitive data in cookies or local storage
  • Never expose your JWT secret key
  • Avoid using OAuth2 without proper token validation
  • Don't implement RBAC without proper role management

Quiz

Question 1 of 21

What command initializes a new Node.js project with npm?

  • npm init
  • node init
  • yarn create
  • express init