Extending Types

From bibbleWiki
Jump to navigation Jump to search

Extending Types

This page documents how to extend Express request types using TypeScript declaration merging, how to attach a `requestId` to each incoming request, and how to integrate Azure authentication in a clean, layered architecture.

1. Extending Express Request Types

TypeScript allows you to extend existing library types using declaration merging. This is useful when you want to attach additional metadata to the Express `Request` object.

Example: Adding requestId to Express Request

Create a file such as:

presentation/express/types/express.d.ts

Add the following:

import "express-serve-static-core";

declare module "express-serve-static-core" {
    interface Request {
        requestId?: string;
    }
}

This augments the existing Express `Request` interface so that TypeScript recognises `req.requestId` throughout your application.

2. Adding a Request ID Middleware

A `requestId` helps correlate logs, errors, and traces.

Middleware

import { v4 as uuid } from "uuid";

export function requestId(req, res, next) {
    const id = uuid();
    req.requestId = id;
    res.setHeader("X-Request-ID", id);
    next();
}

Usage Order in Express

app.use(requestId);
app.use(requestLogger);
app.use(morganConfig);
app.use(corsMiddleware);
app.use(express.json());
app.use("/api", router);
app.use(globalErrorHandler);

The request ID must be registered early so all downstream middleware and handlers can access it.

3. Structured Request Logging

A simple logger that includes the `requestId`:

export function requestLogger(req, res, next) {
    const start = Date.now();

    res.on("finish", () => {
        const duration = Date.now() - start;

        console.log(JSON.stringify({
            requestId: req.requestId,
            method: req.method,
            path: req.originalUrl,
            status: res.statusCode,
            durationMs: duration
        }));
    });

    next();
}

4. Global Error Handler

The global error handler belongs in the Presentation layer. It converts application-level errors into HTTP responses.

import {
    AppValidationError,
    AppDatabaseError,
    AppUnknownError
} from "@dvdrental_api_ts/application/errors";

export function globalErrorHandler(err, req, res, next) {
    const requestId = req.requestId;

    if (err instanceof AppValidationError) {
        return res.status(400).json({
            error: "ValidationError",
            message: err.message,
            requestId
        });
    }

    if (err instanceof AppDatabaseError) {
        return res.status(500).json({
            error: "DatabaseError",
            message: "A database error occurred",
            requestId
        });
    }

    if (err instanceof AppUnknownError) {
        return res.status(500).json({
            error: "UnknownError",
            message: "An unexpected error occurred",
            requestId
        });
    }

    return res.status(500).json({
        error: "InternalServerError",
        message: "Internal server error",
        requestId
    });
}

5. Azure Authentication Example

This example demonstrates how to authenticate incoming API requests using Azure Active Directory (Entra ID) and validate JWT tokens.

Installing Dependencies

npm install @azure/msal-node jsonwebtoken jwks-rsa

Azure JWT Validation Middleware

import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";

const client = jwksClient({
    jwksUri: `https://login.microsoftonline.com/<tenant-id>/discovery/v2.0/keys`
});

function getKey(header, callback) {
    client.getSigningKey(header.kid, (err, key) => {
        const signingKey = key.getPublicKey();
        callback(null, signingKey);
    });
}

export function azureAuth(requiredScope) {
    return (req, res, next) => {
        const authHeader = req.headers.authorization;
        if (!authHeader) {
            return res.status(401).json({ message: "Missing Authorization header" });
        }

        const token = authHeader.replace("Bearer ", "");

        jwt.verify(
            token,
            getKey,
            {
                audience: "<client-id>",
                issuer: `https://login.microsoftonline.com/<tenant-id>/v2.0`
            },
            (err, decoded) => {
                if (err) {
                    return res.status(401).json({ message: "Invalid token" });
                }

                if (requiredScope && !decoded.scp?.includes(requiredScope)) {
                    return res.status(403).json({ message: "Insufficient scope" });
                }

                req.user = decoded;
                next();
            }
        );
    };
}

Protecting a Route

router.get("/secure-data", azureAuth("api.read"), (req, res) => {
    res.json({ message: "Secure data", user: req.user });
});

6. Summary

This page covered:

  • Extending Express request types using declaration merging
  • Adding a `requestId` to each request
  • Structured logging with correlation IDs
  • A global error handler in the Presentation layer
  • Azure authentication using JWT validation

These patterns help maintain a clean architecture while adding observability, traceability, and secure authentication to your Express application.