Building an HTTP API
Build routes with Effect's HTTP modules and write each handler as a VibeLang function. This example creates users, validates JSON bodies, and maps expected failures to HTTP status codes.
Define the body and the service
// users.vibe
// run: provide UsersLive { Console.log(JSON.stringify(Users.create("Ada"))); const repeated = Effect.result(Users.create("Ada")); if (repeated.isFailure()) Console.log(repeated.failure.name) }
export schema CreateUser { name: string }
export error NameTaken { name: string }
export service Users {
create(name: string): { id: string; name: string }!NameTaken
}
export const UsersLive = Layer.effect(Users) {
const names = new Set<string>()
let nextId = 0
return {
create(name) {
if (names.has(name)) throw new NameTaken({ name })
names.add(name)
nextId++
return { id: String(nextId), name }
}
}
}
// Output: {"id":"1","name":"Ada"}
// Output: AdaTypes in this example
CreateUser- const CreateUser: Struct<{ readonly name: String; }>
name- (property) name: String
error- class NameTaken
NameTaken- class NameTaken
name- (property) name: string
service- abstract class Users
Users- abstract class Users
create- (method) Users.create(name: string): { readonly id: string; readonly name: string; }!NameTaken!
name- (parameter) name: string
id- (property) id: string
UsersLive- const UsersLive: Layer<Users, never, never>Layer: provides
Users; needs nothing. Layer- (alias) module "effect/Layer"
effect- const effect: <Users, { create(name: string): { id: string; name: string; }!NameTaken!; }, never, never>(service: Key<Users, { create(name: string): { id: string; name: string; }!NameTaken!; }>, effect: { create(name: string): { id: string; name: string; }!NameTaken!; }!!) => Layer<Users, never, never>Constructs a layer from an effect that produces a single service.
names- const names: Set<string>
Set- var Set: new <string>(iterable?: Iterable<string> | null | undefined) => Set<string>
nextId- let nextId: number
create- (method) create(name: string): { id: string; name: string; }!NameTaken!
has- (method) Set<string>.has(value: string): boolean
NameTaken- constructor NameTaken<{ readonly name: string; }>(args: { readonly name: string; }): NameTaken
add- (method) Set<string>.add(value: string): Set<string>Appends a new element with a specified value to the end of the Set.
String- var String: (value?: any) => stringAllows manipulation and formatting of text strings and determination and location of substrings within strings.
schema CreateUser { name: string } declares a struct schema and its type: it accepts a JSON object with a string name. Users.create declares NameTaken as an expected failure. The layer owns the set of names and the ID counter, so each layer build starts with isolated state.
Decode the request and map failures
// routes.vibe
// run: Effect.scoped { const app = HttpRouter.toHttpEffect(App); const statuses: number[] = []; for (const body of [{ name: "Ada" }, { name: "Ada" }, { name: 123 }]) { const request = HttpServerRequest.fromClientRequest(HttpClientRequest.bodyJson(HttpClientRequest.post("http://local/users"), body)); statuses.push(Effect.provideService(app, HttpServerRequest.HttpServerRequest, request).status) }; Console.log(JSON.stringify(statuses)) }
import { HttpClientRequest, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http"
import { CreateUser, NameTaken, Users, UsersLive } from "./users.vibe"
export function createUser() {
try {
const input = HttpServerRequest.schemaBodyJson(CreateUser)
const user = Users.create(input.name)
return HttpServerResponse.json(user, { status: 201 })
} catch (error) {
return match (error) {
when NameTaken: HttpServerResponse.text(`Name already registered: ${error.name}`, { status: 409 });
when Schema.SchemaError: HttpServerResponse.text("Expected a JSON object with a string name", { status: 400 });
default: throw error;
}
}
}
export const Routes = HttpRouter.add("POST", "/users", createUser())
export const App = HttpRouter.provideRequest(UsersLive)(Routes)
// Output: [201,409,400]Types in this example
HttpClientRequest- (alias) module "effect/http/HttpClientRequest"
HttpRouter- (alias) module "effect/http/HttpRouter"
HttpServerRequest- (alias) module "effect/http/HttpServerRequest"
HttpServerResponse- (alias) module "effect/http/HttpServerResponse"
CreateUser- (alias) const CreateUser: Struct<{ readonly name: String; }> (alias) interface CreateUser
NameTaken- (alias) class NameTaken
Users- (alias) abstract class Users
UsersLive- (alias) const UsersLive: Layer<Users, never, never>Layer: provides
Users; needs nothing. createUser- function createUser(): HttpServerResponse.HttpServerResponse!HttpBodyError | HttpServerError!HttpServerRequest.HttpServerRequest | Users
input- const input: { readonly name: string; }
schemaBodyJson- const schemaBodyJson: <{
readonly name: string;
}, never>(schema: ConstraintDecoder<{
readonly name: string;
}, never>, options?: (ParseOptions & JsonOptions) | undefined) => {
readonly name: string;
}!HttpServerError | SchemaError!HttpServerRequest.HttpServerRequestRuns here and returns
{ readonly name: string; }.Reads the current request body as JSON and decodes it with the supplied schema. CreateUser- (alias) const CreateUser: Struct<{ readonly name: String; }>
user- const user: { readonly id: string; readonly name: string; }
create- (method) Users.create(name: string): {
readonly id: string;
readonly name: string;
}!NameTaken!Runs here and returns
{ readonly id: string; readonly name: string; }. name- (property) name: string
json- const json: (body: unknown, options?: HttpServerResponse.Options.WithContentType | undefined) => HttpServerResponse.HttpServerResponse!HttpBodyError!Runs here and returns
HttpServerResponse.HttpServerResponse.Creates a JSON HTTP response. status- (property) status?: number | undefined
error- var error: HttpBodyError | HttpServerError | NameTaken | SchemaError
text- const text: (body: string, options?: HttpServerResponse.Options.WithContentType) => HttpServerResponse.HttpServerResponseCreates an HTTP response whose body is a string.
error- var error: NameTaken
name- (property) Error.name: string
Schema- (alias) module "effect/Schema"
error- var error: HttpBodyError | HttpServerError
Routes- const Routes: Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError> | HttpRouter.Request<"Requires", Users>>Layer: provides nothing; needs
HttpRouterandRequest<"Error", HttpBodyError>andRequest<"Error", HttpServerError>andRequest<"Requires", Users>. add- const add: <HttpBodyError | HttpServerError, HttpServerRequest.HttpServerRequest | Users>(method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "QUERY", path: HttpRouter.PathInput, handler: (HttpServerResponse.HttpServerResponse!HttpBodyError | HttpServerError!HttpServerRequest.HttpServerRequest | Users) | HttpServerResponse.HttpServerResponse | ..., options?: { readonly uninterruptible?: boolean | undefined; }) => Layer<...>Create a layer that adds a single route to the HTTP router.
App- const App: Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>>Layer: provides nothing; needs
HttpRouterandRequest<"Error", HttpBodyError>andRequest<"Error", HttpServerError>. provideRequest- const provideRequest: <Users, never, never>(layer: Layer<Users, never, never>) => <A, E, R>(self: Layer<A, E, R>) => Layer<A, E, Exclude<R, HttpRouter.Request<"Requires", Users>>>Provides request-level dependencies to some routes.
The JSON decoder runs at the call and returns the validated body. Users.create runs next, and the response is JSON with status 201. A repeated name becomes 409; a body that fails the schema becomes 400. The match handles each error class it names, and default: throw error passes any other failure on to the HTTP boundary.
catch sees expected failures only. It does not catch defects or interruption. Keep error-to-status mapping explicit so a programming defect cannot become a successful response.
HttpRouter.add receives createUser() by name, as a program that runs on each request. HttpRouter.provideRequest supplies Users to route execution. Calling HttpRouter.toHttpEffect in a test will therefore use the same service implementation as the server.
Start the Node server
// server.vibe
// run: provide Logger.layer([]) { Effect.scoped { Layer.build(server(0)); Console.log("server acquired") }; Console.log("server released") }
import { HttpRouter } from "effect/http"
import { NodeHttpServer } from "@effect/platform-node"
import { createServer } from "node:http"
import { App } from "./routes.vibe"
function server(port: number) {
const Server = NodeHttpServer.layer(createServer, { port })
return Layer.provide(HttpRouter.serve(App), Server)
}
const Live = server(3000)
export function main() {
Layer.launch(Live)
}
// Output: server acquired
// Output: server releasedTypes in this example
HttpRouter- (alias) module "effect/http/HttpRouter"
NodeHttpServer- (alias) module "@effect/platform-node/NodeHttpServer"
createServer- (alias) function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(requestListener?: RequestListener<Request, Response>): Server<Request, Response> (alias) function createServer<Request extends typeof IncomingMessage = typeof IncomingMessage, Response extends typeof ServerResponse = typeof ServerResponse>(options: ServerOptions<Request, Response>, requestListener?: RequestListener<Request, Response>): Server<Request, Response>Returns a new instance of Server.
App- (alias) const App: Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>>Layer: provides nothing; needs
HttpRouterandRequest<"Error", HttpBodyError>andRequest<"Error", HttpServerError>. function- function server(port: number): Layer<never, ServeError, never>!!
server- function server(port: number): Layer<never, ServeError, never>Plain JavaScript: VibeLang code calls it directly. TypeScript callers see
Effect<Layer<never, ServeError, never>, never, never>. port- (parameter) port: number
Server- const Server: Layer<Generator | HttpPlatform | HttpServer | NodeServices, ServeError, never>Layer: provides
ChildProcessSpawnerandCryptoandFileSystemandGeneratorandHttpPlatformandHttpServerandPathandStdioandTerminal; needs nothing; fails withServeError. layer- const layer: (evaluate: LazyArg<Server>, options: NodeHttpServer.Options) => Layer.Layer<HttpServer | NodeServices | HttpPlatform | Generator, ServeError>Provides a Node
HttpServertogether with the Node HTTP platform, ETag, and core platform services required to serve requests. port- (property) ListenOptions.port?: number | undefined
Layer- (alias) module "effect/Layer"
provide- const provide: <HttpServer, never, never, never, ServeError, Generator | HttpPlatform | HttpServer | NodeServices>(self: Layer<never, never, HttpServer>, that: Layer<Generator | HttpPlatform | HttpServer | NodeServices, ServeError, never>) => Layer<never, ServeError, never>Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.
serve- const serve: <never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>, unknown, never>(appLayer: Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>>, options?: { readonly routerConfig?: Partial<RouterConfig> | undefined; readonly disableLogger?: boolean | undefined; readonly disableListenLog?: boolean; readonly middleware?: ... | undefined; } | undefined) => Layer<...>Runs the provided application layer as an HTTP server.
Live- const Live: Layer<never, ServeError, never>Layer: provides nothing; needs nothing; fails with
ServeError. main- function main(): void!ServeError!
launch- const launch: <never, ServeError, never>(self: Layer<never, ServeError, never>) => !ServeError!Runs here and returns
never.Builds this layer and keeps it alive until the returned effect is interrupted.
Pass createServer as a value. NodeHttpServer.layer runs the Node API and owns its lifetime; calling createServer() directly in .vibe is a platform API error. HttpRouter.serve builds the serving layer, and Layer.launch keeps it alive until shutdown.
The module top level constructs schemas, routes, and layers. node server.js starts the application: a module that exports a main without parameters ends with if (import.meta.main) NodeRuntime.runMain(main()), so importing it, from a test or another module, starts nothing.
Test a route without a socket
// routes.test.vibe
import { assert, it } from "@effect/vitest"
import { HttpClientRequest, HttpRouter, HttpServerRequest } from "effect/http"
import { App } from "./routes.vibe"
function request(body: unknown) {
const client = HttpClientRequest.bodyJson(HttpClientRequest.post("http://local/users"), body)
return HttpServerRequest.fromClientRequest(client)
}
it.effect("validates bodies and rejects duplicate names") {
Effect.scoped {
const app = HttpRouter.toHttpEffect(App)
const created = Effect.provideService(app, HttpServerRequest.HttpServerRequest, request({ name: "Ada" }))
const conflict = Effect.provideService(app, HttpServerRequest.HttpServerRequest, request({ name: "Ada" }))
const bad = Effect.provideService(app, HttpServerRequest.HttpServerRequest, request({ name: 123 }))
assert.deepStrictEqual([created.status, conflict.status, bad.status], [201, 409, 400])
}
}
// Output:Types in this example
assert- (alias) const assert: Chai.Assert
it- (alias) const it: Vitest.Methods<never, {}>Vitest test API extended with Effect test helpers.
HttpClientRequest- (alias) module "effect/http/HttpClientRequest"
HttpRouter- (alias) module "effect/http/HttpRouter"
HttpServerRequest- (alias) module "effect/http/HttpServerRequest"
App- (alias) const App: Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>>Layer: provides nothing; needs
HttpRouterandRequest<"Error", HttpBodyError>andRequest<"Error", HttpServerError>. request- function request(body: unknown): HttpServerRequest.HttpServerRequest!HttpBodyError!
body- (parameter) body: unknown
client- const client: HttpClientRequest.HttpClientRequest
bodyJson- const bodyJson: (self: HttpClientRequest.HttpClientRequest, body: unknown) => HttpClientRequest.HttpClientRequest!HttpBodyError!Runs here and returns
HttpClientRequest.HttpClientRequest.Encodes a value as a JSON request body and sets it on the request, failing withHttpBodyErrorif encoding fails. post- const post: (url: string | URL, options?: HttpClientRequest.Options.NoUrl) => HttpClientRequest.HttpClientRequestCreates a
POSTrequest for the specified URL. fromClientRequest- const fromClientRequest: (request: HttpClientRequest.HttpClientRequest) => HttpServerRequest.HttpServerRequestCreates an
HttpServerRequestview of anHttpClientRequest. effect- (property) Vitest.MethodsNonLive<never, {}>.effect: <void, HttpBodyError | HttpServerError>(name: string, self: Vitest.TestFunction<void, HttpBodyError | HttpServerError, Scope, [TestContext]>, timeout?: number | TestOptions) => void
Effect- (alias) module "effect/Effect"
scoped- const scoped: <void, HttpBodyError | HttpServerError, Scope>(self: void!HttpBodyError | HttpServerError!Scope) => void!HttpBodyError | HttpServerError!Runs here and returns
void.Runs an effect with a scope that closes when the effect completes. app- const app: HttpServerResponse.HttpServerResponse!HttpBodyError | HttpServerError!HttpServerRequest.HttpServerRequest | Scope
toHttpEffect- const toHttpEffect: <never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>>(appLayer: Layer<never, never, HttpRouter.HttpRouter | HttpRouter.Request<"Error", HttpBodyError> | HttpRouter.Request<"Error", HttpServerError>>) => (HttpServerResponse.HttpServerResponse!HttpBodyError | HttpServerError!HttpServerRequest.HttpServerRequest | Scope)!!ScopeRuns here and returns
Effect<HttpServerResponse.HttpServerResponse, HttpBodyError | HttpServerError, HttpServerRequest.HttpServerRequest | Scope>.Builds an application layer with a router and returns the router as an HTTP handler effect. created- const created: HttpServerResponse.HttpServerResponse
provideService- const provideService: <HttpServerResponse.HttpServerResponse, HttpBodyError | HttpServerError, HttpServerRequest.HttpServerRequest | Scope, HttpServerRequest.HttpServerRequest, HttpServerRequest.HttpServerRequest>(self: HttpServerResponse.HttpServerResponse!HttpBodyError | HttpServerError!HttpServerRequest.HttpServerRequest | Scope, service: Key<...>, implementation: HttpServerRequest.HttpServerRequest) => ...!!Runs here and returns
HttpServerResponse.HttpServerResponse.Provides one concrete service implementation to an effect. HttpServerRequest- const HttpServerRequest: Service<HttpServerRequest.HttpServerRequest, HttpServerRequest.HttpServerRequest>Service tag for the active server-side HTTP request.
request- function request(body: unknown): HttpServerRequest.HttpServerRequest!HttpBodyError!Runs here and returns
HttpServerRequest.HttpServerRequest. name- (property) name: string
conflict- const conflict: HttpServerResponse.HttpServerResponse
bad- const bad: HttpServerResponse.HttpServerResponse
name- (property) name: number
deepStrictEqual- (method) Chai.Assert.deepStrictEqual<number[]>(actual: number[], expected: number[], message?: string): voidAlias to deepEqual
status- (property) HttpServerResponse.status: number
HttpRouter.toHttpEffect builds a request program without starting a server. Hold that program as app, then provide a request for each execution. The scope shares the service layer across these requests and releases it after the test. A new test scope builds a fresh user registry.
Import assertions from @effect/vitest; the compiler trusts its re-exports. Use the installation guide to set up the compiler and test plugin. The complete project, including its Vite configuration, is in examples/http-api.