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.
Related
Last updated on