Skip to content

Latest commit

 

History

History
237 lines (190 loc) · 5.95 KB

File metadata and controls

237 lines (190 loc) · 5.95 KB
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

Guideline

Build middleware as composable Effect functions that wrap handlers, adding cross-cutting concerns like logging, authentication, and error handling.


Rationale

Middleware provides separation of concerns:

  1. Reusability - Write once, apply everywhere
  2. Composability - Stack multiple middlewares
  3. Testability - Test each middleware in isolation
  4. Clarity - Handlers focus on business logic

Good Example

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)

Middleware Order

Request
   ↓
Logging (start)
   ↓
Request ID
   ↓
Timeout
   ↓
CORS
   ↓
Error Handling
   ↓
Handler
   ↓
Error Handling (catch)
   ↓
CORS (headers)
   ↓
Timeout (check)
   ↓
Request ID (add header)
   ↓
Logging (end)
   ↓
Response

Common Middleware

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

Best Practices

  1. Order matters - Auth before rate limit, error handling outer
  2. Keep focused - One concern per middleware
  3. Type-safe - Use Effect types for requirements
  4. Testable - Test each middleware independently