Skip to content

Part 2: Mount the HTTP API

Continue from Part 1. Keep Better Auth’s routes public while building your application’s API alongside them.

src/api.ts
import * as Schema from "effect/Schema";
import * as HttpApi from "effect/unstable/httpapi/HttpApi";
import * as HttpApiEndpoint from "effect/unstable/httpapi/HttpApiEndpoint";
import * as HttpApiGroup from "effect/unstable/httpapi/HttpApiGroup";
export class PublicApi extends HttpApiGroup.make("public").add(
HttpApiEndpoint.get("health", "/api/health", {
success: Schema.Struct({ ok: Schema.Boolean }),
}),
) {}
export class AppApi extends HttpApi.make("app").add(PublicApi) {}

The schema declares the response shape. No authentication is required for this endpoint.

src/http.ts
import * as Http from "alchemy/Http";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder";
import { AppApi } from "./api.ts";
const PublicLive = HttpApiBuilder.group(AppApi, "public", (handlers) =>
handlers.handle("health", () => Effect.succeed({ ok: true })),
);
export const HttpLive = HttpApiBuilder.layer(AppApi).pipe(
Layer.provide(PublicLive),
Layer.provide(Http.Platform),
);

The handler supplies the declared response. Alchemy’s HTTP platform layer provides the services needed to build the API on a Worker.

src/worker.ts
import * as HttpRouter from "effect/unstable/http/HttpRouter";
import { HttpLive } from "./http.ts";
Effect.gen(function* () {
yield* Auth;
return { fetch: Effect.succeed(HttpServerResponse.text("Ready")) };
const fetch = yield* HttpRouter.toHttpEffect(HttpLive);
return { fetch };

Build the router once during Worker construction. Remove the now-unused HttpServerResponse import.

src/worker.ts
import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest";
Effect.gen(function* () {
yield* Auth;
const auth = yield* Auth;
const fetch = yield* HttpRouter.toHttpEffect(HttpLive);
return { fetch };
return {
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const pathname = request.url.split("?")[0];
if (pathname === "/api/auth" || pathname.startsWith("/api/auth/")) {
return yield* auth.fetch;
}
return yield* fetch;
}),
};

Better Auth handles its own endpoints and response cookies. Other paths go through the Effect HTTP API.

Terminal window
curl "$DEV_URL/api/health"
{"ok":true}
Terminal window
curl "$DEV_URL/api/auth/get-session"
null

An anonymous request has no session. A database or authentication failure is an error, not an anonymous session.

Continue to Part 3: Build sign-in.