Skip to content
Core Framework

Middleware & Pipeline

Official Subatom.js documentation for third-party developers.

Middleware & Pipeline System

Subatom's middleware and pipeline system provides multiple points for intercepting and modifying requests and responses. Understand the differences between middleware, transformers, interceptors, and serializers.

Middleware Basics

Middleware are functions that have access to request and response objects and can modify them or respond early.

Middleware Signature

type MiddlewareHandler = (
  req: IRequest,
  res: IResponse,
  next: NextFunction
) => unknown | Promise<unknown>;

type NextFunction = () => Promise<void>;

Global Middleware

Global middleware runs on every request:

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next();
});

app.use(async (req, res, next) => {
  const start = Date.now();
  await next();
  const duration = Date.now() - start;
  res.set("X-Response-Time", `${duration}ms`);
});

Path-Scoped Middleware

Middleware attached to a path prefix:

app.use("/api", (req, res, next) => {
  // Only runs for /api/* requests
  req.locals.apiVersion = "v1";
  next();
});

app.use("/admin", requireAdmin, (req, res, next) => {
  // Only runs for /admin/* requests
  // After requireAdmin middleware
  req.locals.role = "admin";
  next();
});

Route-Scoped Middleware

Middleware attached to specific routes:

app.get("/protected", {
  middleware: [
    (req, res, next) => {
      // Only runs for this route
      if (!req.headers.authorization) {
        res.status(401).json({ error: "No auth" });
        return;
      }
      next();
    },
  ],
  controller: (ctx) => {
    ctx.res.json({ protected: true });
  },
});

Common Middleware Patterns

Logging Middleware

app.use((req, res, next) => {
  const start = Date.now();
  const originalEnd = res.raw.end;

  res.raw.end = function (...args: unknown[]) {
    const duration = Date.now() - start;
    console.log({
      timestamp: new Date().toISOString(),
      method: req.method,
      path: req.path,
      statusCode: res.statusCode,
      duration,
    });
    return originalEnd.apply(res.raw, args);
  };

  next();
});

Authentication Middleware

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

  if (!token) {
    res.status(401).json({ error: "No token" });
    return;
  }

  try {
    const user = await verifyJWT(token);
    req.user = user;
    next();
  } catch (err) {
    res.status(401).json({ error: "Invalid token" });
  }
};

app.use(requireAuth);

CORS Middleware

const cors = (options = {}) => {
  return (req, res, next) => {
    res.set("Access-Control-Allow-Origin", options.origin || "*");
    res.set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
    res.set("Access-Control-Allow-Headers", "Content-Type,Authorization");

    if (req.method === "OPTIONS") {
      res.status(200).send();
      return;
    }

    next();
  };
};

app.use(cors({ origin: "https://example.com" }));

Rate Limiting Middleware

const rateLimit = (options = {}) => {
  const store = new Map<string, number[]>();
  const limit = options.max || 100;
  const windowMs = options.windowMs || 60 * 1000;

  return (req, res, next) => {
    const key = req.ip;
    const now = Date.now();
    const timestamps = store.get(key) || [];
    const withinWindow = timestamps.filter((t) => t > now - windowMs);

    if (withinWindow.length >= limit) {
      res.status(429).json({ error: "Too many requests" });
      return;
    }

    withinWindow.push(now);
    store.set(key, withinWindow);
    next();
  };
};

app.use(rateLimit({ max: 100, windowMs: 60 * 1000 }));

Body Parser Middleware

const bodyParser = (maxSize = "1mb") => {
  return async (req, res, next) => {
    if (["GET", "HEAD"].includes(req.method)) {
      next();
      return;
    }

    try {
      req.body = await req.json();  // Subatom helper
      next();
    } catch (err) {
      res.status(400).json({ error: "Invalid JSON" });
    }
  };
};

app.use(bodyParser());

Error Middleware

Error middleware has 4 parameters for handling errors:

type ErrorMiddlewareHandler = (
  error: Error,
  req: IRequest,
  res: IResponse,
  next: NextFunction
) => unknown | Promise<unknown>;

Basic Error Handler

app.useError((err, req, res, next) => {
  console.error(err);
  res.status(500).json({
    error: "Internal Server Error",
    code: "INTERNAL_ERROR",
  });
});

Multi-Level Error Handling

// Handle validation errors
app.useError((err, req, res, next) => {
  if (err instanceof UnprocessableEntityError) {
    res.status(422).json({
      error: "Validation failed",
      details: err.details,
    });
    return;
  }
  next(err);
});

// Handle not found errors
app.useError((err, req, res, next) => {
  if (err instanceof NotFoundError) {
    res.status(404).json({ error: "Not found" });
    return;
  }
  next(err);
});

// Fallback error handler
app.useError((err, req, res) => {
  res.status(500).json({ error: "Internal Server Error" });
});

Pipeline Modifiers

Beyond middleware, Subatom provides transformers, interceptors, and serializers for lifecycle hooks.

Transformers

Transformers hook into request/response lifecycle with priority-based ordering:

interface ITransformer {
  name?: string;
  priority?: number;  // Lower runs first
  beforeRequest?: (ctx: IPipelineContext) => unknown | Promise<unknown>;
  afterRequest?: (data: unknown, ctx: IPipelineContext) => unknown | Promise<unknown>;
  beforeResponse?: (ctx: IPipelineContext) => unknown | Promise<unknown>;
  afterResponse?: (ctx: IPipelineContext) => unknown | Promise<unknown>;
}

beforeRequest

Runs after middleware, before route handler:

app.transformer({
  name: "auth-transformer",
  priority: -10,  // Run first
  beforeRequest: async (ctx) => {
    const token = ctx.req.headers.authorization?.replace("Bearer ", "");
    if (token) {
      ctx.req.user = await verifyToken(token);
    }
  },
});

afterRequest

Runs after route handler completes:

app.transformer({
  name: "response-wrapper",
  afterRequest: (data, ctx) => {
    return {
      success: true,
      data,
      timestamp: new Date().toISOString(),
    };
  },
});

beforeResponse

Runs after serialization, before sending to client:

app.transformer({
  name: "add-headers",
  beforeResponse: (ctx) => {
    ctx.res.set("X-Response-Time", ctx.state.duration);
    ctx.res.set("Cache-Control", "public, max-age=3600");
  },
});

afterResponse

Runs after response sent (for logging, cleanup):

app.transformer({
  name: "response-logger",
  afterResponse: (ctx) => {
    console.log({
      path: ctx.req.path,
      method: ctx.req.method,
      statusCode: ctx.res.statusCode,
      duration: Date.now() - ctx.state.startTime,
    });
  },
});

Interceptors

Interceptors run before route handler with middleware-style next() pattern:

interface IInterceptor {
  name?: string;
  priority?: number;
  intercept: (
    ctx: IPipelineContext,
    next: () => Promise<unknown>
  ) => unknown | Promise<unknown>;
}

Intercept Pattern

app.intercept({
  name: "timing-interceptor",
  async intercept(ctx, next) {
    const start = Date.now();
    try {
      await next();  // Call route handler
    } finally {
      ctx.state.duration = Date.now() - start;
    }
  },
});

app.intercept({
  name: "validation-interceptor",
  async intercept(ctx, next) {
    // Pre-processing before handler
    validateRequest(ctx.req);
    await next();
    // Post-processing after handler
  },
});

Serializers

Serializers handle final response formatting:

interface ISerializer {
  name?: string;
  priority?: number;
  contentType?: string;
  serialize: (data: unknown, ctx: IPipelineContext) => unknown | Promise<unknown>;
}

Multiple Format Serializers

app.serializer({
  name: "json-serializer",
  contentType: "application/json",
  serialize: (data) => {
    return JSON.stringify(data);
  },
});

app.serializer({
  name: "xml-serializer",
  contentType: "application/xml",
  serialize: (data) => {
    return objectToXml(data);
  },
});

app.serializer({
  name: "csv-serializer",
  contentType: "text/csv",
  serialize: (data: unknown[]) => {
    return convertToCsv(data);
  },
});

Pipeline Execution Order

Request received
    ↓
1. Global Middleware (app.use)
    ↓
2. Group Middleware
    ↓
3. Route Middleware
    ↓
4. Transformers (beforeRequest) - priority order
    ↓
5. Validation
    ↓
6. Interceptors (intercept) - priority order
    ↓
7. Route Handler (controller)
    ↓
8. Transformers (afterRequest) - priority order
    ↓
9. Serializers - priority order
    ↓
10. Transformers (beforeResponse) - priority order
    ↓
11. Send Response
    ↓
12. Transformers (afterResponse) - priority order

Best Practices

1. Order Matters

// Correct order
app.use(corsMiddleware);          // 1. CORS first
app.use(bodyParserMiddleware);    // 2. Parse body
app.use(authMiddleware);          // 3. Authenticate
app.use(rateLimitMiddleware);     // 4. Rate limit
app.use(loggerMiddleware);        // 5. Logging

2. Use Priority for Transformers

// Auth transformer should run first
app.transformer({
  name: "auth",
  priority: -100,  // Highest priority
  beforeRequest: loadUser,
});

// Logging transformer should run last
app.transformer({
  name: "logging",
  priority: 100,  // Lowest priority
  afterResponse: logRequest,
});

3. Error Handling in Middleware

app.use(async (req, res, next) => {
  try {
    await risky();
    next();
  } catch (err) {
    next(err);  // Pass to error middleware
  }
});

4. Short-Circuit on Error

app.use((req, res, next) => {
  if (req.method !== "GET" && !req.get("content-type")) {
    res.status(400).json({ error: "Content-Type required" });
    return;  // Don't call next()
  }
  next();
});

5. Middleware Composition

const compose = (...middlewares) => {
  return async (req, res, next) => {
    let index = 0;
    const dispatch = async (i) => {
      if (i <= index) throw new Error("next() called multiple times");
      index = i;
      if (i < middlewares.length) {
        try {
          await middlewares[i](req, res, () => dispatch(i + 1));
        } catch (err) {
          next(err);
        }
      } else {
        next();
      }
    };
    return dispatch(0);
  };
};

app.use(
  compose(
    authMiddleware,
    loggingMiddleware,
    validationMiddleware
  )
);

Common Pitfalls

❌ Not Calling next()

// Wrong: Request hangs
app.use((req, res, next) => {
  console.log("middleware");
  // Forgot next()!
});

// Correct
app.use((req, res, next) => {
  console.log("middleware");
  next();
});

❌ Calling next() Multiple Times

// Wrong: Undefined behavior
app.use((req, res, next) => {
  next();
  next();  // Double call!
});

// Correct: Call once
app.use((req, res, next) => {
  if (condition) {
    doThing();
  }
  next();  // Single call
});

❌ Mixing Middleware and Transformers

// Wrong: Mixing concerns
app.transformer({
  beforeRequest: (ctx) => {
    // Don't put middleware logic in transformers
    if (!ctx.req.headers.authorization) {
      ctx.res.status(401).send();
      return;
    }
  },
});

// Correct: Use middleware for auth
app.use((req, res, next) => {
  if (!req.headers.authorization) {
    res.status(401).send();
    return;
  }
  next();
});

Next Steps