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)
)