skills/tstelzer/skills/effect-http-server

effect-http-server

SKILL.md

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

Weekly Installs
27
Repository
tstelzer/skills
First Seen
Feb 16, 2026
Installed on
cursor27
opencode25
codex25
claude-code23
github-copilot23
gemini-cli21