# Handlers

A handler implements one endpoint. You write handlers with `HttpApiBuilder.group`,
which gives you a builder whose `.handle(name, fn)` method is **type-checked
against the API definition**: the endpoint name must exist, and the handler's
inputs and outputs must match the endpoint's schemas. The result is a `Layer`
that provides that group's routes.

## A service for the business logic

Keep request handling thin: put the real work in a [service](https://effect.plants.sh/services-and-layers/)
so it stays testable and reusable. Note the use of `Effect.fn` for methods that
return Effects, and a single wrapper error (`UsersError`) so the service's
failure channel stays small.

```ts
// server/Users.ts
import { Context, Effect, Layer, Ref } from "effect"
import { User, UserId } from "../domain/User.ts"
import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts"

export class Users extends Context.Service<Users, {
  list(search: string | undefined): Effect.Effect<Array<User>, UsersError>
  getById(id: UserId): Effect.Effect<User, UsersError>
  create(input: { readonly name: string; readonly email: string }): Effect.Effect<User, UsersError>
}>()("acme/Users") {
  static readonly layer = Layer.effect(
    Users,
    Effect.gen(function*() {
      const users = new Map<number, User>()
      const nextId = yield* Ref.make(1)

      const list = Effect.fn("Users.list")(function*(search: string | undefined) {
        const all = Array.from(users.values())
        if (search === undefined || search.length === 0) return all
        if (search.length < SearchQueryTooShort.minimumLength) {
          // Wrap domain errors in the single UsersError reason union
          return yield* new UsersError({ reason: new SearchQueryTooShort() })
        }
        const q = search.toLowerCase()
        return all.filter((u) => u.name.toLowerCase().includes(q))
      })

      const getById = Effect.fn("Users.getById")(function*(id: UserId) {
        const user = users.get(id)
        if (user === undefined) {
          return yield* new UsersError({ reason: new UserNotFound() })
        }
        return user
      })

      const create = Effect.fn("Users.create")(function*(input: { readonly name: string; readonly email: string }) {
        const id = yield* Ref.getAndUpdate(nextId, (n) => n + 1)
        const user = new User({ id: UserId.make(id), ...input })
        users.set(user.id, user)
        return user
      })

      return Users.of({ list, getById, create })
    })
  )
}
```

## Implementing the group

`HttpApiBuilder.group(api, groupName, build)` builds handlers for one group. The
`build` function runs as an Effect — `yield*` your dependencies, then return the
chained `.handle(...)` calls. Each handler receives the **decoded** inputs
(`params`, `query`, `payload`, `headers`) and returns an Effect producing the
endpoint's success type.

```ts
// server/Users/http.ts
import { Effect, Layer } from "effect"
import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
import { Api } from "../../api/Api.ts"
import { CurrentUser } from "../../api/Authorization.ts"
import { Users } from "../Users.ts"

export const UsersApiHandlers = HttpApiBuilder.group(
  Api,
  "users",
  Effect.fn(function*(handlers) {
    // Resolve dependencies once for the whole group
    const users = yield* Users

    return handlers
      .handle("list", ({ query }) =>
        users.list(query.search).pipe(
          // The `list` endpoint declares no errors, so any failure here must be
          // turned into an unexpected defect (becomes a 500).
          Effect.orDie
        ))

      .handle("getById", ({ params }) =>
        // `params.id` is already decoded to a branded UserId
        users.getById(params.id).pipe(
          // Surface only the UserNotFound reason as a typed failure; treat the
          // rest as defects.
          Effect.catchReasons("UsersError", {
            UserNotFound: (e) => Effect.fail(e)
          }, Effect.die)
        ))

      .handle("create", ({ payload }) =>
        users.create(payload).pipe(Effect.orDie))

      .handle(
        "search",
        Effect.fn(function*({ payload }) {
          // Built-in HttpApiError types behave like any tagged error
          if (payload.search === "bad-request") {
            return yield* new HttpApiError.RequestTimeout()
          }
          return yield* users.list(payload.search).pipe(
            // Re-fail one reason, die on the others
            Effect.catchReason(
              "UsersError",
              "SearchQueryTooShort",
              Effect.fail,
              Effect.die
            )
          )
        })
      )

      // Endpoints with no inputs receive an empty object. This handler simply
      // returns a service provided by middleware (see the auth page).
      .handle("me", () => CurrentUser)
  })
).pipe(
  // Provide everything the handlers need
  Layer.provide(Users.layer)
)
```
**Note:** The handler builder enforces the contract both ways. If you `.handle` an
endpoint name that does not exist, or return a value that does not match the
endpoint's `success` schema, or fail with an error not declared in `error`, the
group will not compile.

## Matching the endpoint's error channel

A handler may only fail with errors the endpoint **declared**. Anything else must
become a defect (an unexpected `500`). The combinators that bridge your service
errors to the endpoint's error channel are the standard ones from
[Error Management](https://effect.plants.sh/error-management/):

- `Effect.orDie` — the endpoint declares no errors; convert any failure to a defect.
- `Effect.catch` — the catch-all handler (the v4 replacement for `catchAll`).
- `Effect.catchTag` / `Effect.catchTags` — handle specific tagged errors.
- `Effect.catchReason` / `Effect.catchReasons` — handle reasons inside a wrapper
  error like `UsersError` without unwrapping it.

If you prefer to flatten a wrapper error's reasons up to the top level so you can
use `catchTag`/`catchTags`, use `Effect.unwrapReason`:

```ts
import { Effect } from "effect"

users.create(payload).pipe(
  // Lift UsersError's reasons (UserNotFound | SearchQueryTooShort) to the top
  Effect.unwrapReason("UsersError"),
  Effect.catchTags({
    UserNotFound: Effect.die,
    SearchQueryTooShort: Effect.die
  })
)
```

## Wiring the group into the API

`HttpApiBuilder.group` returns a `Layer`. You provide all your group layers to
`HttpApiBuilder.layer(Api)` when assembling the server — covered next in
[Serving & clients](https://effect.plants.sh/http-api/serving-and-clients/). A trivial group can be
implemented inline:

```ts
import { Effect } from "effect"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api/Api.ts"

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

To secure these endpoints and inject the current user, continue to
[Middleware & auth](https://effect.plants.sh/http-api/middleware-and-auth/).