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/sdkAPSO_API_URL=https://your-service.apso.cloud
APSO_API_KEY=replace-with-a-server-side-keyimport '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
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
'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 };
}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:
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:
- Authenticate the user with Better Auth or another identity provider.
- Read the session in the Server Component, Server Action, or route handler.
- Resolve the user’s workspace and role on the server.
- Send trusted identity or tenant headers only when the generated backend is configured to validate them.
- 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_URLandAPSO_API_KEYto the deployment environment. - Confirm the key has only the permissions required by this frontend.
- Keep
lib/apso.tsbehindserver-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.