Middleware

Composable request/response transformations using Effect

The Pattern

Middleware in Gello are functions that wrap handlers, transforming requests or responses. They compose naturally using pipe and can access/provide context.

import * as HttpMiddleware from "@effect/platform/HttpMiddleware"

// Built-in logging middleware
const withLogging = HttpMiddleware.logger

// Apply to your app
const HttpApp = pipe(
  HttpRouter.toHttpApp(AppRouter),
  withLogging
)

Custom Middleware

const withTiming = HttpMiddleware.make((app) =>
  Effect.gen(function* () {
    const start = Date.now()
    const response = yield* app
    const duration = Date.now() - start

    return response.pipe(
      HttpServerResponse.setHeader("X-Response-Time", `${duration}ms`)
    )
  })
)

Authentication Middleware

class CurrentUser extends Context.Tag("CurrentUser")<
  CurrentUser,
  { id: string; email: string; role: string }
>() {}

const withAuth = HttpMiddleware.make((app) =>
  Effect.gen(function* () {
    const request = yield* HttpServerRequest.HttpServerRequest
    const auth = request.headers["authorization"]

    if (!auth?.startsWith("Bearer ")) {
      return HttpServerResponse.json(
        { error: "Unauthorized" },
        { status: 401 }
      )
    }

    const token = auth.slice(7)
    const user = yield* verifyToken(token)

    // Provide user to downstream handlers
    return yield* app.pipe(Effect.provideService(CurrentUser, user))
  })
)

CORS Middleware

const cors = (options: { origins: string | string[] }) =>
  HttpMiddleware.make((app) =>
    Effect.gen(function* () {
      const request = yield* HttpServerRequest.HttpServerRequest
      const origin = request.headers["origin"]

      const response = yield* app

      const allowedOrigins = Array.isArray(options.origins)
        ? options.origins
        : [options.origins]

      if (origin && (allowedOrigins.includes("*") || allowedOrigins.includes(origin))) {
        return response.pipe(
          HttpServerResponse.setHeader("Access-Control-Allow-Origin", origin),
          HttpServerResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE"),
          HttpServerResponse.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization")
        )
      }

      return response
    })
  )

Rate Limiting

const withRateLimit = (limit: number, window: Duration.Duration) =>
  HttpMiddleware.make((app) =>
    Effect.gen(function* () {
      const request = yield* HttpServerRequest.HttpServerRequest
      const redis = yield* Redis
      const ip = request.headers["x-forwarded-for"] ?? "unknown"
      const key = `ratelimit:${ip}`

      const count = yield* Effect.tryPromise(() => redis.incr(key))

      if (count === 1) {
        yield* Effect.tryPromise(() =>
          redis.expire(key, Duration.toSeconds(window))
        )
      }

      if (count > limit) {
        return HttpServerResponse.json(
          { error: "Too many requests" },
          { status: 429 }
        )
      }

      return yield* app
    })
  )

Composing Middleware

// Middleware compose left-to-right
const HttpApp = pipe(
  HttpRouter.toHttpApp(AppRouter),
  withLogging,        // 1. Log request
  withTiming,         // 2. Track duration
  cors({ origins: "*" }), // 3. Add CORS headers
  withRateLimit(100, Duration.minutes(1)) // 4. Rate limit
)

Route-Specific Middleware

// Apply middleware to specific routes
const PublicRouter = pipe(
  HttpRouter.empty,
  HttpRouter.get("/health", healthCheck),
  HttpRouter.get("/docs", getDocs)
)

const ProtectedRouter = pipe(
  HttpRouter.empty,
  HttpRouter.get("/me", getProfile),
  HttpRouter.patch("/settings", updateSettings)
).pipe(withAuth)

// Merge routers
const AppRouter = pipe(
  HttpRouter.empty,
  HttpRouter.mount("/api", PublicRouter),
  HttpRouter.mount("/api", ProtectedRouter)
)