Skip to Content
Apso is in public beta. Get started

Integrate Next.js

Use Next.js Server Components, Server Actions, and route handlers as a backend-for-frontend layer over an Apso-generated API. This keeps the service API key out of the browser and gives the frontend a focused server boundary.

Install and configure

npm install @apso/sdk
.env.local
APSO_API_URL=https://your-service.apso.cloud APSO_API_KEY=replace-with-a-server-side-key
lib/apso.ts
import 'server-only'; import { ApsoClientFactory } from '@apso/sdk'; export const apso = ApsoClientFactory.getClient({ baseURL: process.env.APSO_API_URL!, apiKey: process.env.APSO_API_KEY!, retry: true, });

Do not prefix the API key with NEXT_PUBLIC_. Import this client only from Server Components, Server Actions, route handlers, and other server-only modules.

Read in a server component

app/projects/page.tsx
import { apso } from '@/lib/apso'; interface Project { id: string; name: string; status: 'Active' | 'Archived'; } export default async function ProjectsPage() { const projects = await apso.entity('Projects') .where({ status: { $eq: 'Active' } }) .orderBy({ created_at: 'DESC' }) .findMany<Project[]>(); return ( <ul> {projects.map((project) => ( <li key={project.id}>{project.name}</li> ))} </ul> ); }

Mutate in a server action

app/projects/actions.ts
'use server'; import { revalidatePath } from 'next/cache'; import { apso } from '@/lib/apso'; export async function createProject(formData: FormData) { const name = String(formData.get('name') || '').trim(); if (!name) { return { error: 'Project name is required.' }; } await apso.entity('Projects').create({ name, status: 'Active', }); revalidatePath('/projects'); return { error: null }; }
app/projects/create-project-form.tsx
import { createProject } from './actions'; export function CreateProjectForm() { return ( <form action={createProject}> <label htmlFor="project-name">Project name</label> <input id="project-name" name="name" required /> <button type="submit">Create project</button> </form> ); }

Expose a narrow route handler

Use a route handler when a Client Component needs dynamic data from the BFF:

app/api/projects/route.ts
import { NextResponse } from 'next/server'; import { apso } from '@/lib/apso'; export async function GET() { const projects = await apso.entity('Projects') .orderBy({ created_at: 'DESC' }) .limit(20) .findMany(); return NextResponse.json(projects, { headers: { 'Cache-Control': 'private, max-age=30' }, }); }

The Client Component calls /api/projects. It never receives the Apso service URL or API key.

Carry end-user identity

An API key identifies the calling service. User authorization still needs an end-user identity and tenant context. A common BFF flow is:

  1. Authenticate the user with Better Auth or another identity provider.
  2. Read the session in the Server Component, Server Action, or route handler.
  3. Resolve the user’s workspace and role on the server.
  4. Send trusted identity or tenant headers only when the generated backend is configured to validate them.
  5. Return the minimum data the frontend route needs.

See Authentication and Multi-Tenancy before exposing protected data.

Error handling

export async function getProject(id: string) { try { return await apso.entity('Projects') .where({ id }) .findOne(); } catch (error) { console.error('Apso project request failed', { id, error }); throw new Error('Unable to load project.'); } }

Keep detailed transport errors in server logs. Return stable, user-facing errors from Server Actions and route handlers.

Deployment checklist

  • Add APSO_API_URL and APSO_API_KEY to the deployment environment.
  • Confirm the key has only the permissions required by this frontend.
  • Keep lib/apso.ts behind server-only.
  • Add timeouts and retry only for idempotent requests.
  • Log request failures without logging credentials or sensitive response bodies.
  • Verify tenant isolation with two test users from different workspaces.
Last updated on