1---
2title: "Part 4: Protect API endpoints"
3description: Resolve the session in Effect HTTP API middleware and provide a typed CurrentUser service.
4sidebar:
5 order: 4
6---
7
8Continue from [Part 3](/better-auth/tutorial/part-3). Protect a new `/api/me` endpoint without blocking sign-in or the public health check.
9
10## Describe the current user
11
12```typescript
13// src/current-user.ts
14import * as Context from "effect/Context";
15import * as Schema from "effect/Schema";
16
17export const User = Schema.Struct({
18 id: Schema.String,
19 name: Schema.String,
20 email: Schema.String,
21});
22
23export class CurrentUser extends Context.Service<
24 CurrentUser,
25 typeof User.Type
26>()("app/CurrentUser") {}
27```
28
29Handlers receive only the user fields they need. Session tokens stay out of API responses.
30
31## Declare an unauthorized response
32
33```typescript
34// src/middleware.ts
35import * as Schema from "effect/Schema";
36
37export class Unauthorized extends Schema.TaggedError<Unauthorized>()(
38 "Unauthorized",
39 {},
40 { httpApiStatus: 401 },
41) {}
42```
43
44A request without a session returns a typed `401`.
45
46## Distinguish authentication failures
47
48```typescript
49// Append to src/middleware.ts
50export class AuthenticationUnavailable extends Schema.TaggedError<AuthenticationUnavailable>()(
51 "AuthenticationUnavailable",
52 {},
53 { httpApiStatus: 503 },
54) {}
55```
56
57A failed session lookup returns `503`, not `401`. Its response exposes no database errors or credentials.
58
59## Declare the middleware
60
61```diff lang="typescript"
62 // src/middleware.ts
63+import * as HttpApiMiddleware from "effect/http-api/HttpApiMiddleware";
64+import { CurrentUser } from "./current-user.ts";
65+
66+export class Authentication extends HttpApiMiddleware.Service<
67+ Authentication,
68+ { provides: CurrentUser }
69+>()("app/Authentication", {
70+ error: [Unauthorized, AuthenticationUnavailable],
71+}) {}
72```
73
74The middleware declares the service it provides and the errors callers can receive.
75
76## Implement session authentication
77
78```diff lang="typescript"
79 // src/middleware.ts
80+import { RuntimeContext } from "alchemy";
81+import * as Effect from "effect/Effect";
82+import * as Layer from "effect/Layer";
83+import { Auth } from "./auth.ts";
84
85 error: [Unauthorized, AuthenticationUnavailable],
86-}) {}
87+}) {
88+ static readonly layer = Layer.effect(
89+ Authentication,
90+ Effect.gen(function* () {
91+ const auth = yield* Auth;
92+ return (httpEffect) => Effect.gen(function* () {
93+ const session = yield* auth.getSession().pipe(
94+ Effect.mapError(() => new AuthenticationUnavailable()),
95+ Effect.catchDefect(() => Effect.fail(new AuthenticationUnavailable())),
96+ );
97+ if (session === null) return yield* Effect.fail(new Unauthorized());
98+ return yield* Effect.provideService(httpEffect, CurrentUser, {
99+ id: session.user.id,
100+ name: session.user.name,
101+ email: session.user.email,
102+ });
103+ }).pipe(Effect.provide(RuntimeContext.phantom));
104+ }),
105+ );
106+}
107```
108
109The Auth service is captured once; the session is read inside each request. `RuntimeContext.phantom` adapts Alchemy's runtime-only methods to the native HTTP middleware boundary; it does not create a session or bypass authentication.
110
111## Protect one API group
112
113```diff lang="typescript"
114 // src/api.ts
115+import { User } from "./current-user.ts";
116+import { Authentication } from "./middleware.ts";
117+
118+export class PrivateApi extends HttpApiGroup.make("private")
119+ .add(HttpApiEndpoint.get("me", "/api/me", { success: User }))
120+ .middleware(Authentication) {}
121
122-export class AppApi extends HttpApi.make("app").add(PublicApi) {}
123+export class AppApi extends HttpApi.make("app")
124+ .add(PublicApi)
125+ .add(PrivateApi) {}
126```
127
128Only the private group requires authentication. Better Auth's own routes and the public group remain accessible while signed out.
129
130## Read the user in a handler
131
132```diff lang="typescript"
133 // src/http.ts
134+import { CurrentUser } from "./current-user.ts";
135+
136+const PrivateLive = HttpApiBuilder.group(AppApi, "private", (handlers) =>
137+ handlers.handle("me", () => CurrentUser),
138+);
139
140 export const HttpLive = HttpApiBuilder.layer(AppApi).pipe(
141- Layer.provide(PublicLive),
142+ Layer.provide(Layer.mergeAll(PublicLive, PrivateLive)),
143 Layer.provide(Http.Platform),
144 );
145```
146
147The middleware supplies `CurrentUser` before the handler runs. The handler does not parse cookies itself.
148
149## Provide the middleware implementation
150
151```diff lang="typescript"
152 // src/http.ts
153+import { Authentication } from "./middleware.ts";
154
155 export const HttpLive = HttpApiBuilder.layer(AppApi).pipe(
156 Layer.provide(Layer.mergeAll(PublicLive, PrivateLive)),
157+ Layer.provide(Authentication.layer),
158 Layer.provide(Http.Platform),
159 );
160```
161
162The Worker's existing `Auth.layer` supplies the middleware's Auth dependency. No additional database connection configuration is needed.
163
164## Call the protected endpoint
165
166Add a button to the existing page:
167
168```diff lang="html"
169 <!-- public/index.html -->
170 <button id="session" type="button">Read session</button>
171+<button id="me" type="button">Call protected API</button>
172```
173
174Wire it to the API:
175
176```typescript
177// Append to src/ui.ts
178const readMe = async () => {
179 const response = await fetch("/api/me");
180 if (response.status === 401) return show("Sign in first.");
181 if (!response.ok) return show("Unable to read your account.");
182 const user = await response.json();
183 show(`API user: ${user.email}`);
184};
185
186document.querySelector("#me")!.addEventListener("click", () => {
187 void readMe().catch(() => show("Unable to reach the server."));
188});
189```
190
191Rebuild the browser bundle:
192
193```sh
194bun build ./src/ui.ts --target browser --outdir ./public
195```
196
197## Verify the boundary
198
199```sh
200curl -i "$DEV_URL/api/me"
201```
202
203```http
204HTTP/1.1 401 Unauthorized
205Content-Type: application/json
206
207{"_tag":"Unauthorized"}
208```
209
210In the browser, sign in and select **Call protected API**; it should show your email. Sign out and repeat; it should ask you to sign in while `/api/health` still returns `200`.
211
212Continue to [Part 5: Add GitHub sign-in](/better-auth/tutorial/part-5).