@alphinex/api-laravel
v1.1.0
Published
Laravel Sanctum/Reverb/Horizon adapter implementing the @alphinex/api ApiClient contract.
Readme
@alphinex/api-laravel
Laravel Sanctum/Reverb adapter that implements @alphinex/api's ApiClient contract: CSRF-cookie
or Bearer-token auth, Laravel's 422 validation shape mapped into ApiError.fieldErrors, a
session-expiry event you can wire to @alphinex/auth, and hooks for real-time updates over
Laravel Echo/Reverb.
createLaravelApiClient(options)
The main entry point — wires the right interceptors onto @alphinex/api's createApiClient for
you and returns both the resulting client and an events emitter:
import { createLaravelApiClient } from "@alphinex/api-laravel";
export const { client, events } = createLaravelApiClient({
baseUrl: "https://api.example.com",
});
const invoices = await client.get("/invoices");mode defaults to "cookie" (Sanctum SPA session auth) and attaches the X-XSRF-TOKEN header
from the XSRF-TOKEN cookie automatically. For Bearer-token auth (mobile/3rd-party clients), pass
mode: "bearer" with a tokenStore and a refresh function — concurrent 401s share a single
in-flight refresh call, and each queued request replays with the new token:
import { createLaravelApiClient, createInMemoryTokenStore } from "@alphinex/api-laravel";
const tokenStore = createInMemoryTokenStore();
const { client, events } = createLaravelApiClient({
baseUrl: "https://api.example.com",
mode: "bearer",
bearer: {
tokenStore,
refresh: async (fetchFn) => {
const response = await fetchFn("https://api.example.com/refresh", { method: "POST" });
const { token } = await response.json();
return token;
},
},
});Every request/validation-error/session-expiry interceptor described below is already included;
use extraRequestInterceptors/extraResponseInterceptors to append your own on top, and
fetchFn to override the global fetch (mocking, tests). If you need fine-grained control
instead of the bundled defaults, call the individual pieces below directly against
@alphinex/api's createApiClient.
Composing with createApiClient yourself
createLaravelApiClient is really just a preset over @alphinex/api's createApiClient and the
pieces below — reach for the pieces directly if you need a different combination:
import { createApiClient } from "@alphinex/api";
import {
createCsrfInterceptor,
createValidationErrorInterceptor,
createSessionEvents,
createSessionExpiryInterceptor,
} from "@alphinex/api-laravel";
const events = createSessionEvents();
const client = createApiClient({
baseUrl: "https://api.example.com",
requestInterceptors: [createCsrfInterceptor()],
responseInterceptors: [
createValidationErrorInterceptor(),
createSessionExpiryInterceptor(events),
],
});CSRF (createCsrfInterceptor, ensureCsrfCookie)
createCsrfInterceptor(cookieName?, headerName?) reads the XSRF-TOKEN cookie (defaults match
Sanctum) and attaches it as X-XSRF-TOKEN on outgoing requests — already included by
createLaravelApiClient in "cookie" mode. Call ensureCsrfCookie(baseUrl, fetchFn?, path?) once
per session, before the first mutating request, to have the browser fetch and store that cookie:
import { ensureCsrfCookie } from "@alphinex/api-laravel";
await ensureCsrfCookie("https://api.example.com"); // GET /sanctum/csrf-cookieSession events (createSessionEvents, createSessionExpiryInterceptor)
createSessionEvents() returns an EventEmitter<ApiLaravelEvents> emitting a
"session-expired" event (with { reason: "cookie-401" | "bearer-refresh-failed" }) whenever the
session is no longer valid. Create one instance per client and pass it to both
createLaravelApiClient (or createSessionExpiryInterceptor directly) and to @alphinex/auth's
AuthProvider so the app signs the user out and redirects automatically:
import { AuthProvider } from "@alphinex/auth";
import { createLaravelApiClient } from "@alphinex/api-laravel";
const { client, events } = createLaravelApiClient({ baseUrl: "https://api.example.com" });
<AuthProvider
adapter={authAdapter}
subscribeSessionExpired={(handler) => events.on("session-expired", () => handler())}
>
<App />
</AuthProvider>;Validation errors (createValidationErrorInterceptor, mapLaravelValidationErrors)
createValidationErrorInterceptor() intercepts Laravel's 422 responses and throws an ApiError
with fieldErrors already mapped from { message, errors: { field: string[] } } into
Record<string, string[]>. mapLaravelValidationErrors(body) is the underlying mapping function,
exported separately if you need it outside the interceptor pipeline (e.g. mapping a body you
already parsed):
import { ApiError } from "@alphinex/api";
try {
await client.post("/invoices", payload);
} catch (error) {
if (error instanceof ApiError && error.fieldErrors) {
console.log(error.fieldErrors.amount?.[0]); // "The amount field is required."
}
}Bearer auth (createBearerAuthInterceptor, createBearerRefreshInterceptor, createInMemoryTokenStore)
The building blocks behind mode: "bearer" above, exported for custom wiring.
createInMemoryTokenStore(initialToken?) implements the TokenStore contract
(getToken/setToken) in memory — swap it for a persisted store (secure storage, etc.) in a real
app. createBearerAuthInterceptor(tokenStore) attaches Authorization: Bearer <token> to every
request. createBearerRefreshInterceptor(options) retries a 401 once, sharing a single in-flight
refresh across concurrent failed requests, and emits "session-expired" (reason
"bearer-refresh-failed") if the refresh call itself fails.
Laravel Echo (useEchoChannel, useEchoQuerySync)
useEchoChannel(echo, channelName, event, handler) subscribes to a Reverb channel/event for the
lifetime of the component and cleans up on unmount. echo is typed against the structural
EchoLike/EchoChannelLike interfaces (this package has no hard dependency on laravel-echo —
any real Echo instance satisfies them):
import { useEchoChannel } from "@alphinex/api-laravel";
import Echo from "laravel-echo";
function InvoiceNotifications({ echo }: { echo: Echo }) {
useEchoChannel(echo, "invoices", "InvoiceCreated", (payload) => {
console.log("New invoice", payload);
});
return null;
}useEchoQuerySync(echo, channelName, event, queryClient, queryKey) is the common case built on
top: invalidate a TanStack Query key whenever the event fires, keeping WebSocket concerns out of
component code:
import { useEchoQuerySync } from "@alphinex/api-laravel";
import { queryKeys } from "@alphinex/api";
import { useQueryClient } from "@tanstack/react-query";
function InvoicesLive({ echo }: { echo: Echo }) {
const queryClient = useQueryClient();
useEchoQuerySync(
echo,
"invoices",
"InvoiceCreated",
queryClient,
queryKeys.resource("invoices").lists()
);
return null;
}See documentation/ARCHITECTURE.md for the full package contract, dependency rules, and roadmap placement.
