VibeLang

Warning: VibeLang is v0. The language will change in major, breaking ways. It is not suitable for production use.

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

VibeLang
// 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: Ada

Types 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

VibeLang
// 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 HttpRouter and Request<"Error", HttpBodyError> and Request<"Error", HttpServerError> and Request<"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 HttpRouter and Request<"Error", HttpBodyError> and Request<"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

VibeLang
// 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 released

Types 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 HttpRouter and Request<"Error", HttpBodyError> and Request<"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 ChildProcessSpawner and Crypto and FileSystem and Generator and HttpPlatform and HttpServer and Path and Stdio and Terminal; needs nothing; fails with ServeError.
layer
const layer: (evaluate: LazyArg<Server>, options: NodeHttpServer.Options) => Layer.Layer<HttpServer | NodeServices | HttpPlatform | Generator, ServeError>Provides a Node HttpServer together 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

VibeLang
// 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 HttpRouter and Request<"Error", HttpBodyError> and Request<"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 with HttpBodyError if encoding fails.
post
const post: (url: string | URL, options?: HttpClientRequest.Options.NoUrl) => HttpClientRequest.HttpClientRequestCreates a POST request for the specified URL.
fromClientRequest
const fromClientRequest: (request: HttpClientRequest.HttpClientRequest) => HttpServerRequest.HttpServerRequestCreates an HttpServerRequest view of an HttpClientRequest.
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.