Skip to main content

Admin API & Typed SDK Workflow

The admin platform uses a generate-once-consume-everywhere pattern for its HTTP contract. The backend emits an OpenAPI 3.0 spec, the frontend turns it into a typed Axios SDK, and stores call the SDK instead of stringly-typed axios.get('/api/...') URLs.

Toolchain

SideToolOutput
Backenddedoc/scrambleOpenAPI 3.0 JSON via php artisan scramble:export
Frontend@hey-api/openapi-tsTyped SDK (src/api/generated/) and TypeScript types
Frontend@hey-api/client-axiosAxios runtime, sharing src/lib/axios.ts

Tenant services follow the exact same pattern with their own swagger/{service}.json; see the tenant-frontend openapi-ts.config.ts.

Day-to-day workflow

Adding or changing an admin endpoint

  1. Edit a controller in main-backend/app/Http/Controllers/{Tenant/,Admin/,…}/.

  2. (Optional but recommended) annotate the method with PHPDoc — Scramble uses these to enrich the spec:

    /**
    * Activate a suspended tenant.
    *
    * @response array{success:bool, data:Tenant}
    */
    public function activate(Tenant $tenant): JsonResponse { ... }
  3. Regenerate the spec and copy it to the frontend:

    # main-backend
    php artisan scramble:export
    cp docs/api.json ../main-frontend/swagger/admin.json

    # main-frontend
    npm run api:generate
  4. The new method appears immediately in src/api/generated/sdk.gen.ts. TypeScript will surface every store and component that needs to update.

Calling the SDK from a Pinia store

import { sdk } from '@/api'
import type { Tenant } from '@/types/api'

export const useTenantStore = defineStore('tenant', () => {
async function fetchTenant(id: number): Promise<Tenant | null> {
const { data } = await sdk.tenantsShow({ path: { tenant: String(id) } })
return data?.data ?? null
}
return { fetchTenant }
})

The SDK validates the path/query/body shapes at compile time. Renames or removed routes immediately turn into red squiggles in every consumer.

Why we still keep src/types/api.ts

Scramble generates response types from controller PHPDoc, but most controllers do not yet have @response annotations. While we backfill those, hand-curated domain types in src/types/api.ts are the source of truth for stores. Once a domain has full PHPDoc coverage, the hand-typed interface can be dropped in favour of the generated *Response type.

Authentication

src/api/index.ts wires the generated client to share the application-wide axios instance:

import api from '@/lib/axios'
import { client } from './generated/client.gen'

client.setConfig({ axios: api, baseURL: import.meta.env.VITE_API_URL + '/api/admin' })

That means every SDK call participates in:

  • Bearer token injection from localStorage
  • 401-triggered refresh-token flow with deduped concurrent retries
  • Global 4xx/5xx toasts via useNotificationStore
  • 422 toasts with the first validation error
  • Accept-Language header from localStorage.language

Local development tips

  • Run the backend on :8002 (the default) before npm run api:export; otherwise the curl will fail.

  • npm run api:generate re-emits the SDK deterministically; commit the generated files so frontend builds don't depend on the backend running.

  • The OpenAPI spec is written to main-backend/docs/api.json. Add the file to .gitignore if you don't want to commit the backend artifact (the canonical copy is main-frontend/swagger/admin.json).

  • If php artisan fails with "opentelemetry extension must be loaded", export the spec with the OTel auto-instrumentation disabled:

    OTEL_PHP_AUTOLOAD_ENABLED=false OTEL_SDK_DISABLED=true php artisan scramble:export

Migration story

Pre-refactor, the frontend had four parallel HTTP wrappers (lib/axios.ts, utils/axios.ts, utils/api.ts, composables/useApi.ts) and 113 : any types in stores. The post-refactor state:

  • One axios wrapper (src/lib/axios.ts)
  • Typed SDK (src/api/generated/) wired to that wrapper
  • Domain types (src/types/api.ts) replacing the worst any clusters
  • 50%+ reduction in : any occurrences across the codebase

See main-backend split for the matching backend refactor.