Skip to main content
The HTTP presentation layer exposes the coffee shop’s use cases as a REST API built on Effect’s HttpApi module. Every endpoint is fully type-safe from request parsing through to the response schema — no runtime casting required.

Starting the server

1

Install dependencies

From the repository root, install all workspace dependencies.
bun install
2

Start the HTTP server

Run the http script from the repository root (or from inside backend/).
bun run http
The server reads the COFFEE_HTTP_PORT environment variable and falls back to port 3000 when it is not set.
3

Verify the server is up

curl http://localhost:3000/health
# {"status":"ok"}
bun run dev also starts the HTTP server alongside the frontend Vite dev server through Turborepo. Use bun run http when you only need the backend.

Base URL

http://localhost:3000
Set COFFEE_HTTP_PORT to any available port before starting if 3000 is already taken:
COFFEE_HTTP_PORT=4000 bun run http

API groups

Effect’s HttpApi model organises endpoints into named groups. The coffee shop API defines three:

HealthApi

A single top-level health-check endpoint with no prefix. Used by load balancers and readiness probes.

MenuApi

Menu retrieval endpoints rooted at /menu.

OrdersApi

Full order lifecycle — create, list, fetch, and all barista status transitions — rooted at /orders.
The groups are composed into a single CoffeeHttpApi declaration and served as one router:
export class CoffeeHttpApi extends HttpApi.make("coffee-order-api")
  .add(HealthApi)
  .add(MenuApi)
  .add(OrdersApi) {}

Endpoints

HealthApi

MethodPathDescription
GET/healthReturns {"status":"ok"} when the server is running.
curl http://localhost:3000/health
{ "status": "ok" }
MethodPathDescription
GET/menuReturns the full list of drinks available for ordering.
curl http://localhost:3000/menu
[
  {
    "id": "latte",
    "name": "Latte",
    "kind": "espresso",
    "basePriceCents": 450,
    "availableMilks": ["whole", "oat", "almond", "none"],
    "availableTemperatures": ["hot", "iced", "extra-hot"],
    "maxShots": 4
  }
]

OrdersApi

All order endpoints share the /orders prefix.
MethodPathDescription
POST/ordersPlace a new coffee order.
GET/ordersList orders, optionally filtered by status.
GET/orders/:orderIdFetch a single order by ID.
POST/orders/:orderId/start-brewingTransition an order from pending to brewing.
POST/orders/:orderId/mark-readyTransition an order from brewing to ready.
POST/orders/:orderId/pick-upTransition an order from ready to picked-up.
POST/orders/:orderId/cancelCancel a pending or brewing order.

Place an order — POST /orders

The request body follows the PlaceOrderRequest schema. size defaults to medium when omitted.
curl -X POST http://localhost:3000/orders \
  -H "Content-Type: application/json" \
  -d '{"customerName":"Alice","drinkId":"latte","size":"large","milk":"oat"}'
{
  "id": "order-1",
  "customerName": "Alice",
  "drinkId": "latte",
  "drinkName": "Latte",
  "size": "large",
  "milk": "oat",
  "temperature": "hot",
  "shots": 1,
  "status": "pending",
  "priceCents": 585,
  "createdAt": "2026-04-10T09:00:00.000Z"
}

List orders — GET /orders

An optional status query parameter filters by order status.
# All orders
curl http://localhost:3000/orders

# Only pending orders
curl "http://localhost:3000/orders?status=pending"

Get one order — GET /orders/:orderId

curl http://localhost:3000/orders/order-1

Barista status transitions

# Start brewing
curl -X POST http://localhost:3000/orders/order-1/start-brewing

# Mark ready for pickup
curl -X POST http://localhost:3000/orders/order-1/mark-ready

# Mark as picked up
curl -X POST http://localhost:3000/orders/order-1/pick-up

# Cancel the order
curl -X POST http://localhost:3000/orders/order-1/cancel
Each transition endpoint returns the updated CoffeeOrder object. Requests that violate the state machine (e.g., marking a cancelled order as ready) return an InvalidOrderStatusTransitionError.

OpenAPI specification

The server exposes a machine-readable OpenAPI JSON document at:
GET /openapi.json
curl http://localhost:3000/openapi.json | jq .
This is wired up in the HttpApiBuilder.layer call inside api.ts:
HttpApiBuilder.layer(CoffeeHttpApi, { openapiPath: "/openapi.json" })
You can import the spec into any OpenAPI-compatible tool (Insomnia, Postman, Swagger UI) by pointing it at http://localhost:3000/openapi.json.

How Effect HttpApi generates type-safe handlers

Each API group is defined as a class that extends HttpApiGroup.make. Endpoints declare their method, path, request shape, success schema, and possible error schemas in one place:
HttpApiEndpoint.post("create", "/", {
  payload: PlaceOrderRequestSchema,
  success: CoffeeOrderSchema,
  error: [DrinkNotFoundError, InvalidOrderInputError, InternalAppError],
})
Handlers are then bound with HttpApiBuilder.group. The compiler enforces that every declared endpoint has exactly one handler and that the handler returns the correct type:
const OrdersApiLive = HttpApiBuilder.group(CoffeeHttpApi, "orders", (handlers) =>
  handlers
    .handle("create", ({ payload }) => CoffeeOrderApp.use((app) => app.placeOrder(payload)))
    .handle("list",   ({ query })   => CoffeeOrderApp.use((app) => app.listOrders(query)))
    .handle("getById",({ params })  => CoffeeOrderApp.use((app) => app.getOrder(params.orderId)))
    // ...
);
Because the group type is derived from the API definition, a missing handler or a wrong return type is a compile error, not a runtime surprise.

Build docs developers (and LLMs) love