Skip to main content

Overview

HttpApi provides a schema-first approach to building HTTP APIs. Define your API once with Effect Schema, and get:
  • Type-safe handlers and clients
  • Automatic request/response validation
  • Generated OpenAPI documentation
  • End-to-end type safety

Defining an API

Create an API definition with groups and endpoints:
import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"
import { Schema } from "effect"

// Define response schemas
class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
}) {}

// Define endpoint groups
class UsersApi extends HttpApiGroup.make("users")(
  HttpApiEndpoint.get("list", "/users")(
    Schema.Array(User)
  ),
  HttpApiEndpoint.get("getById", "/users/:id")(
    User,
    { path: Schema.Struct({ id: Schema.NumberFromString }) }
  ),
  HttpApiEndpoint.post("create", "/users")(
    User,
    { body: Schema.Struct({ name: Schema.String, email: Schema.String }) }
  )
) {}

class SystemApi extends HttpApiGroup.make("system")(
  HttpApiEndpoint.get("health", "/health")(Schema.Void)
) {}

// Combine groups into a full API
class Api extends HttpApi.make("api")(
  UsersApi,
  SystemApi
) {}

Implementing handlers

Create handlers that match your API definition:
import { Effect, Layer } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"

const UsersApiHandlers = HttpApiBuilder.group(
  Api,
  "users",
  Effect.fn(function*(handlers) {
    const userService = yield* UserService

    return handlers
      .handle("list", () => userService.listUsers())
      .handle("getById", ({ path }) => userService.getUserById(path.id))
      .handle("create", ({ body }) => userService.createUser(body))
  })
)

const SystemApiHandlers = HttpApiBuilder.group(
  Api,
  "system",
  Effect.fn(function*(handlers) {
    return handlers.handle("health", () => Effect.void)
  })
)

Serving the API

Build the API routes and serve them:
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { HttpRouter } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { createServer } from "node:http"

const ApiRoutes = HttpApiBuilder.layer(Api, {
  openapiPath: "/openapi.json"
}).pipe(
  Layer.provide([UsersApiHandlers, SystemApiHandlers])
)

const server = HttpRouter.serve(ApiRoutes).pipe(
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(server).pipe(NodeRuntime.runMain)

Using the typed client

Generate a typed client from your API definition:
import { HttpApiClient } from "effect/unstable/httpapi"
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http"
import { Effect, Layer, Schedule, ServiceMap } from "effect"

class ApiClient extends ServiceMap.Service<
  ApiClient,
  HttpApiClient.ForApi<typeof Api>
>()("app/ApiClient") {
  static readonly layer = Layer.effect(
    ApiClient,
    HttpApiClient.make(Api, {
      transformClient: (client) =>
        client.pipe(
          HttpClient.mapRequest(
            HttpClientRequest.prependUrl("http://localhost:3000")
          ),
          HttpClient.retryTransient({
            schedule: Schedule.exponential(100),
            times: 3
          })
        )
    })
  ).pipe(
    Layer.provide(FetchHttpClient.layer)
  )
}

// Use the client
const program = Effect.gen(function*() {
  const client = yield* ApiClient

  // Fully typed calls
  const users = yield* client.list()
  const user = yield* client.getById({ path: { id: 123 } })
  const newUser = yield* client.create({
    body: { name: "Alice", email: "[email protected]" }
  })
})

OpenAPI documentation

Serving docs with Scalar

import { HttpApiScalar } from "effect/unstable/httpapi"

const DocsRoute = HttpApiScalar.layer(Api, {
  path: "/docs"
})

const AllRoutes = Layer.mergeAll(ApiRoutes, DocsRoute)
Visit /docs to see interactive API documentation.

Customizing OpenAPI output

const ApiRoutes = HttpApiBuilder.layer(Api, {
  openapiPath: "/openapi.json",
  info: {
    title: "My API",
    version: "1.0.0",
    description: "A type-safe API built with Effect"
  }
})

Middleware

Defining middleware

import { HttpApiMiddleware } from "effect/unstable/httpapi"

class Authorization extends HttpApiMiddleware.Tag<Authorization>()("Authorization", {
  failure: Schema.Struct({ _tag: Schema.tag("Unauthorized") }),
  provides: Schema.Struct({ userId: Schema.String })
}) {}

Applying to endpoints

class UsersApi extends HttpApiGroup.make("users")(
  HttpApiEndpoint.get("profile", "/profile")(
    User,
    { middleware: Authorization }
  )
) {}

Implementing middleware

// Server-side
const AuthorizationServer = HttpApiMiddleware.layerServer(
  Authorization,
  Effect.fn(function*({ next, request }) {
    const token = request.headers.get("authorization")
    if (!token) {
      return yield* Effect.fail({ _tag: "Unauthorized" as const })
    }
    return yield* next({ userId: "user-123" })
  })
)

// Client-side
const AuthorizationClient = HttpApiMiddleware.layerClient(
  Authorization,
  Effect.fn(function*({ next, request }) {
    const modifiedRequest = HttpClientRequest.bearerToken(request, "my-token")
    return yield* next(modifiedRequest)
  })
)

Complete example

import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { Effect, Layer, Schema, ServiceMap } from "effect"
import { HttpRouter } from "effect/unstable/http"
import { HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, HttpApi } from "effect/unstable/httpapi"
import { createServer } from "node:http"

class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String
}) {}

class UsersApi extends HttpApiGroup.make("users")(
  HttpApiEndpoint.get("list", "/users")(Schema.Array(User)),
  HttpApiEndpoint.get("getById", "/users/:id")(
    User,
    { path: Schema.Struct({ id: Schema.NumberFromString }) }
  )
) {}

class Api extends HttpApi.make("api")(UsersApi) {}

const UsersApiHandlers = HttpApiBuilder.group(
  Api,
  "users",
  Effect.fn(function*(handlers) {
    return handlers
      .handle("list", () => Effect.succeed([
        { id: 1, name: "Alice" },
        { id: 2, name: "Bob" }
      ]))
      .handle("getById", ({ path }) =>
        Effect.succeed({ id: path.id, name: "Alice" })
      )
  })
)

const ApiRoutes = HttpApiBuilder.layer(Api, {
  openapiPath: "/openapi.json"
}).pipe(
  Layer.provide([UsersApiHandlers])
)

const server = HttpRouter.serve(ApiRoutes).pipe(
  Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)

Layer.launch(server).pipe(NodeRuntime.runMain)

See also

Build docs developers (and LLMs) love