effect-http-server
Structure
HttpApi
├── HttpApiGroup
│ ├── HttpApiEndpoint
│ └── HttpApiEndpoint
Defining Endpoints
import { HttpApiEndpoint, HttpApiSchema, HttpApiError } from "@effect/platform"
import { Schema } from "effect"
const User = Schema.Struct({
id: Schema.Number,
name: Schema.String,
createdAt: Schema.DateTimeUtc
})
const idParam = HttpApiSchema.param("id", Schema.NumberFromString)
// GET with path param (template string syntax)
const getUser = HttpApiEndpoint.get("getUser")`/users/${idParam}`
.addSuccess(User)
.addError(HttpApiError.NotFound) // 404
// GET with URL params
const listUsers = HttpApiEndpoint.get("listUsers", "/users")
.setUrlParams(Schema.Struct({
page: Schema.NumberFromString,
sort: Schema.String
}))
.addSuccess(Schema.Array(User))
// POST with payload
const createUser = HttpApiEndpoint.post("createUser", "/users")
.setPayload(Schema.Struct({ name: Schema.String }))
.addSuccess(User, { status: 201 })
// DELETE
const deleteUser = HttpApiEndpoint.del("deleteUser")`/users/${idParam}`
// PATCH
const updateUser = HttpApiEndpoint.patch("updateUser")`/users/${idParam}`
.setPayload(Schema.Struct({ name: Schema.String }))
.addSuccess(User)
// Headers (keys must be lowercase)
const withHeaders = HttpApiEndpoint.get("withHeaders", "/")
.setHeaders(Schema.Struct({
"x-api-key": Schema.String,
"x-request-id": Schema.String
}))
Grouping & API Assembly
import { HttpApi, HttpApiGroup } from "@effect/platform"
class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {}) {}
class Unauthorized extends Schema.TaggedError<Unauthorized>()("Unauthorized", {}) {}
const usersGroup = HttpApiGroup.make("users")
.add(getUser)
.add(listUsers)
.add(createUser)
.add(deleteUser)
.add(updateUser)
.addError(Unauthorized, { status: 401 }) // group-level error
const api = HttpApi.make("myApi")
.add(usersGroup)
.prefix("/api/v1") // optional prefix
Implementation
import { HttpApiBuilder } from "@effect/platform"
import { Context, Effect, Layer, DateTime } from "effect"
// Service for handlers
class UsersRepo extends Context.Tag("UsersRepo")<UsersRepo, {
findById: (id: number) => Effect.Effect<typeof User.Type>
}>() {}
// Implement group handlers
const usersGroupLive = HttpApiBuilder.group(api, "users", (handlers) =>
Effect.gen(function* () {
const repo = yield* UsersRepo
return handlers
.handle("getUser", ({ path: { id } }) => repo.findById(id))
.handle("listUsers", ({ urlParams: { page, sort } }) =>
Effect.succeed([{ id: 1, name: "John", createdAt: DateTime.unsafeNow() }])
)
.handle("createUser", ({ payload: { name } }) =>
Effect.succeed({ id: 2, name, createdAt: DateTime.unsafeNow() })
)
.handle("deleteUser", ({ path: { id } }) => Effect.void)
.handle("updateUser", ({ path: { id }, payload: { name } }) =>
Effect.succeed({ id, name, createdAt: DateTime.unsafeNow() })
)
// Access raw request if needed (do not echo secrets)
.handle("withHeaders", ({ request }) =>
Effect.succeed(`method: ${request.method}`)
)
})
)
// Combine into API layer
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(usersGroupLive))
Serving
import { HttpApiSwagger, HttpMiddleware, HttpServer } from "@effect/platform"
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
import { createServer } from "node:http"
const ServerLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe(
Layer.provide(HttpApiSwagger.layer()), // /docs
Layer.provide(HttpApiBuilder.middlewareCors()), // CORS
Layer.provide(MyApiLive),
HttpServer.withLogAddress,
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
)
Layer.launch(ServerLive).pipe(NodeRuntime.runMain)
Client
import { HttpApiClient, FetchHttpClient } from "@effect/platform"
const program = Effect.gen(function* () {
const client = yield* HttpApiClient.make(api, { baseUrl: "http://localhost:3000" })
const user = yield* client.users.getUser({ path: { id: 1 } })
})
program.pipe(Effect.provide(FetchHttpClient.layer), Effect.runPromise)
Custom Encoding
// URL-encoded request
const urlEncoded = HttpApiEndpoint.post("urlEncoded", "/form")
.setPayload(
Schema.Struct({ a: Schema.String })
.pipe(HttpApiSchema.withEncoding({ kind: "UrlParams" }))
)
// CSV response
const csv = HttpApiEndpoint.get("csv", "/export")
.addSuccess(
Schema.String.pipe(HttpApiSchema.withEncoding({
kind: "Text",
contentType: "text/csv"
}))
)
Predefined Errors
| Error | Status |
|---|---|
HttpApiError.BadRequest |
400 |
HttpApiError.Unauthorized |
401 |
HttpApiError.Forbidden |
403 |
HttpApiError.NotFound |
404 |
HttpApiError.Conflict |
409 |
HttpApiError.InternalServerError |
500 |
Additional Resources
For deriving http clients For creating middlewares For deriving swagger UIs For multipart uploads For streaming
More from tstelzer/skills
skill-creator
Guide for creating or updating simple, standalone skills with a single SKILL.md file
53effect
Explains basic effect usage and terms. Use when using effect in typescript.
31effect-handbook
Canonical handbook for Effect ecosystem usage in TypeScript. Always use when interacting with Effect code.
28planning
Use when you have a spec or requirements for a multi-step task, before touching code
23zod-v4
Canonical handbook for Zod 4 usage in TypeScript and JavaScript. Use when defining Zod schemas, parsing or validating data, shaping objects, composing unions, applying refinements or transforms or codecs, customizing errors, or generating JSON Schema and metadata.
15answers
Answer-only mode for user Q&A. Use when the user asks a question and explicitly wants a direct answer, explanation, or advice without editing files, changing code, or running scripts.
12