| title |
Compose API Middleware |
| id |
api-middleware |
| skillLevel |
intermediate |
| applicationPatternId |
building-apis |
| summary |
Build reusable middleware for logging, authentication, validation, and more. |
| tags |
building-apis |
middleware |
composition |
|
| rule |
| description |
Use Effect composition to build a middleware pipeline that processes requests. |
|
| author |
PaulJPhilp |
| related |
api-authentication |
api-rate-limiting |
|
| lessonOrder |
2 |
Build middleware as composable Effect functions that wrap handlers, adding cross-cutting concerns like logging, authentication, and error handling.
Middleware provides separation of concerns:
- Reusability - Write once, apply everywhere
- Composability - Stack multiple middlewares
- Testability - Test each middleware in isolation
- Clarity - Handlers focus on business logic
import { Effect, Context, Layer, Duration } from "effect"
import { HttpServerRequest, HttpServerResponse } from "@effect/platform"
// ============================================
// 1. Define middleware type
// ============================================
type Handler<E, R> = Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>
type Middleware<E1, R1, E2 = E1, R2 = R1> = <E extends E1, R extends R1>(
handler: Handler<E, R>
) => Handler<E | E2, R | R2>
// ============================================
// 2. Logging middleware
// ============================================
const withLogging: Middleware<never, HttpServerRequest.HttpServerRequest> =
(handler) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
const startTime = Date.now()
yield* Effect.log(`→ ${request.method} ${request.url}`)
const response = yield* handler
const duration = Date.now() - startTime
yield* Effect.log(`← ${response.status} (${duration}ms)`)
return response
})
// ============================================
// 3. Timing middleware (adds header)
// ============================================
const withTiming: Middleware<never, never> = (handler) =>
Effect.gen(function* () {
const startTime = Date.now()
const response = yield* handler
const duration = Date.now() - startTime
return HttpServerResponse.setHeader(
response,
"X-Response-Time",
`${duration}ms`
)
})
// ============================================
// 4. Error handling middleware
// ============================================
const withErrorHandling: Middleware<unknown, never, never> = (handler) =>
handler.pipe(
Effect.catchAll((error) =>
Effect.gen(function* () {
yield* Effect.logError(`Unhandled error: ${error}`)
return HttpServerResponse.json(
{ error: "Internal Server Error" },
{ status: 500 }
)
})
)
)
// ============================================
// 5. Request ID middleware
// ============================================
class RequestId extends Context.Tag("RequestId")<RequestId, string>() {}
const withRequestId: Middleware<never, never, never, RequestId> = (handler) =>
Effect.gen(function* () {
const requestId = crypto.randomUUID()
const response = yield* handler.pipe(
Effect.provideService(RequestId, requestId)
)
return HttpServerResponse.setHeader(response, "X-Request-Id", requestId)
})
// ============================================
// 6. Timeout middleware
// ============================================
const withTimeout = (duration: Duration.DurationInput): Middleware<never, never> =>
(handler) =>
handler.pipe(
Effect.timeout(duration),
Effect.catchTag("TimeoutException", () =>
Effect.succeed(
HttpServerResponse.json(
{ error: "Request timeout" },
{ status: 504 }
)
)
)
)
// ============================================
// 7. CORS middleware (see separate pattern)
// ============================================
const withCORS = (origin: string): Middleware<never, never> => (handler) =>
Effect.gen(function* () {
const response = yield* handler
return response.pipe(
HttpServerResponse.setHeader("Access-Control-Allow-Origin", origin),
HttpServerResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE"),
HttpServerResponse.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization")
)
})
// ============================================
// 8. Compose middleware
// ============================================
const applyMiddleware = <E, R>(handler: Handler<E, R>) =>
handler.pipe(
withLogging,
withTiming,
withRequestId,
withTimeout("30 seconds"),
withCORS("*"),
withErrorHandling
)
// ============================================
// 9. Usage
// ============================================
const myHandler = Effect.gen(function* () {
const requestId = yield* RequestId
yield* Effect.log(`Processing request ${requestId}`)
return HttpServerResponse.json({ message: "Hello!" })
})
const protectedHandler = applyMiddleware(myHandler)
Request
↓
Logging (start)
↓
Request ID
↓
Timeout
↓
CORS
↓
Error Handling
↓
Handler
↓
Error Handling (catch)
↓
CORS (headers)
↓
Timeout (check)
↓
Request ID (add header)
↓
Logging (end)
↓
Response
| Middleware |
Purpose |
| Logging |
Request/response logging |
| Timing |
Performance measurement |
| Auth |
Token validation |
| Rate Limit |
Abuse prevention |
| CORS |
Cross-origin requests |
| Error |
Global error handling |
| Compression |
Response compression |
| Cache |
Response caching |
- Order matters - Auth before rate limit, error handling outer
- Keep focused - One concern per middleware
- Type-safe - Use Effect types for requirements
- Testable - Test each middleware independently