9. Consume from the frontend
Read from a Server Component
// app/tasks/page.tsx
import { serverApi } from '@/lib/api';
export default async function Page() {
const client = await serverApi();
const { data, error } = await client.GET('/tasks');
if (error || !data) return <p>Failed to load.</p>;
return (
<ul>
{data.items.map((t) => (
<li key={t.id}>{t.title}</li>
))}
</ul>
);
}
serverApi() reads the NextAuth httpOnly cookie via next/headers and attaches the Bearer header server-side: the token never touches client JS.
Mutate via a Server Action
// app/tasks/actions.ts
'use server';
import { serverApi } from '@/lib/api';
import { revalidatePath } from 'next/cache';
export type CreateTaskState = { error: string | null };
export async function createTaskAction(
_prev: CreateTaskState,
formData: FormData,
): Promise<CreateTaskState> {
const client = await serverApi();
const { error } = await client.POST('/tasks', {
body: { title: String(formData.get('title')) },
});
if (error) return { error: 'Create failed' };
revalidatePath('/tasks');
return { error: null };
}
// app/tasks/NewTaskForm.tsx (client island)
'use client';
import { useActionState } from 'react';
import { createTaskAction } from './actions';
export function NewTaskForm() {
const [state, formAction, pending] = useActionState(createTaskAction, {
error: null,
});
return (
<form action={formAction}>
<input name="title" required />
<button type="submit" disabled={pending}>
Add
</button>
{state.error ? <p role="alert">{state.error}</p> : null}
</form>
);
}
openapi-fetch types both halves: body is checked against CreateTaskSchema, data is the typed TaskDto.
Checklist
- Knex migration + row interface in
db/types.ts - Table added to
DELETION_POLICYinsrc/users/deletion-policy.ts, with its erasure wired intoUsersService.ghostDeleteand asserted inusers.service.integration.spec.ts, or a reason it needs none -
*.schemas.tswith zod schemas, DTO classes, extractor -
*.service.tswith data access, lists throughpaginate() -
*.controller.tswith REST decorators, guards,@ApiOkResponse(@ApiCreatedResponsefor aPOSTthat creates a resource) -
*.module.tsregistering controller/service - Module added to
app.module.ts - Routes added to the
POLICYtable insrc/common/route-auth.spec.ts -
npm run build && npx ts-node -r tsconfig-paths/register scripts/dump-openapi.ts, copied to the repository-rootopenapi.json -
npm run generate-typesinfrontend/ - Frontend consumer (RSC + Server Action) wired
-
./verify.shgreen