Skip to Content
Apso is in public beta. Get started

React integration

From a browser-based React application, call either a backend-for-frontend route or the generated REST API with an end-user token. Keep Apso service API keys in a trusted server environment.

Choose the API boundary

Do not put an Apso service API key in a VITE_* environment variable. Vite includes those values in the browser bundle.

  • Use a BFF when the frontend needs service-level access, response shaping, or server-side tenant checks.
  • Use direct REST when the user has an access token and the generated API is configured for that identity provider and browser origin.

BFF request helper

The following client calls a route owned by your application, such as a Next.js route handler or an Express endpoint:

src/lib/api.ts
export async function apiRequest<T>(path: string, init?: RequestInit): Promise<T> { const response = await fetch(`/api${path}`, { ...init, headers: { 'Content-Type': 'application/json', ...init?.headers, }, credentials: 'include', }); if (!response.ok) { throw new Error(`API request failed with ${response.status}`); } return response.json() as Promise<T>; }

The BFF uses @apso/sdk with APSO_API_URL and APSO_API_KEY stored on the server.

Query projects

src/components/project-list.tsx
import { useEffect, useState } from 'react'; import { apiRequest } from '../lib/api'; interface Project { id: number; name: string; status: 'Active' | 'Archived'; } export function ProjectList() { const [projects, setProjects] = useState<Project[]>([]); const [error, setError] = useState<string | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { const controller = new AbortController(); apiRequest<Project[]>('/projects', { signal: controller.signal }) .then(setProjects) .catch((requestError: Error) => { if (requestError.name !== 'AbortError') setError(requestError.message); }) .finally(() => setLoading(false)); return () => controller.abort(); }, []); if (loading) return <p>Loading projects...</p>; if (error) return <p role="alert">{error}</p>; return ( <ul> {projects.map((project) => <li key={project.id}>{project.name}</li>)} </ul> ); }

Create a project

src/components/create-project-form.tsx
import { FormEvent, useState } from 'react'; import { apiRequest } from '../lib/api'; export function CreateProjectForm({ onCreated }: { onCreated: () => void }) { const [name, setName] = useState(''); const [submitting, setSubmitting] = useState(false); async function handleSubmit(event: FormEvent) { event.preventDefault(); setSubmitting(true); try { await apiRequest('/projects', { method: 'POST', body: JSON.stringify({ name, status: 'Active' }), }); setName(''); onCreated(); } finally { setSubmitting(false); } } return ( <form onSubmit={handleSubmit}> <label htmlFor="project-name">Project name</label> <input id="project-name" value={name} onChange={(event) => setName(event.target.value)} required /> <button type="submit" disabled={submitting}>{submitting ? 'Creating...' : 'Create project'}</button> </form> ); }

Direct REST with an end-user token

const response = await fetch(`${import.meta.env.VITE_API_URL}/Projects`, { headers: { Authorization: `Bearer ${accessToken}` }, });

Configure CORS for the exact application origin and store browser sessions in secure, HTTP-only cookies when your authentication architecture permits it.

Production checklist

  • Keep service API keys on a server.
  • Handle loading, empty, error, and retry states.
  • Cancel requests when components unmount.
  • Validate tenant context on the API or BFF.
  • Test 401, 403, 409, and validation responses.
  • Avoid rendering raw backend error details to users.
Last updated on