Use when migrating frontend code from manual API client calls (`api.get`, `api.create`, `api.surveys.get`, `api.dashboards.list`, `new ApiRequest()`) and handwritten TypeScript interfaces to generated API functions and types. Triggers on files importing from `lib/api`, files with `api.get<`, `api.create<`, `api.<entity>.<method>`, manual interface definitions that duplicate backend serializers, or any frontend file that constructs API URLs by hand. Covers the full replacement workflow — finding the generated equivalent, swapping imports, adapting call sites, and removing dead manual types.
PostHog generates TypeScript API client functions and types from Django serializers via the OpenAPI pipeline:
Django serializer → drf-spectacular → OpenAPI JSON → Orval → TypeScript (api.ts + api.schemas.ts)
Generated files live in:
frontend/src/generated/core/api.ts and api.schemas.tsproducts/<product>/frontend/generated/api.ts and api.schemas.tsGenerated types use the Api suffix (DashboardApi, SurveyApi). Handwritten types never do.
This skill guides replacing manual API calls and handwritten types with generated equivalents.
The legacy frontend/src/lib/api.ts (~6000 lines) has three layers, all migration targets:
Domain-specific convenience methods on the api object:
api.surveys.get(id)
api.surveys.create(data)
api.dashboards.list()
api.cohorts.update(id, data)
api.actions.create(data)
These are the most widely used pattern — every entity has its own namespace with CRUD plus custom methods (e.g., api.surveys.getResponsesCount(), api.dashboards.streamTiles()).
api.get<SomeType>(`api/projects/${id}/surveys/`)
api.create<SomeType>(`api/projects/${id}/surveys/`, data)
api.update<SomeType>(url, data)
api.put<SomeType>(url, data)
api.delete(url)
const url = new ApiRequest().surveys().assembleFullUrl()
const response = await api.get(url)
// or directly:
await new ApiRequest().survey(surveyId).withAction('summarize_responses').create({ data })
All three patterns should be replaced with generated functions where available.
api.<entity>.<method>() (e.g., api.surveys.get())api.get<T>(...), api.create<T>(...), etc.new ApiRequest() to build URLs~/types for API response shapesLook at the existing call and extract:
Generated function names follow the {resource}{Action} convention:
surveysList — GET /api/projects/{id}/surveys/
surveysCreate — POST /api/projects/{id}/surveys/
surveysRetrieve — GET /api/projects/{id}/surveys/{id}/
surveysPartialUpdate — PATCH /api/projects/{id}/surveys/{id}/
surveysDestroy — DELETE /api/projects/{id}/surveys/{id}/
Where to search:
frontend/src/generated/core/api.tsproducts/<product>/frontend/generated/api.tsSearch strategies:
api.ts filesget*Url helper functions — every generated function has a URL builder above itapi.schemas.ts for the type name with Api suffixIf no generated function exists, the backend endpoint may lack @extend_schema or @validated_request. Fix the backend first using the improving-drf-endpoints skill, then run hogli build:openapi.
Custom actions (like api.surveys.summarize_responses()) may not have generated equivalents if the backend @action lacks @extend_schema. Check generated files first; if missing, fix the backend.
Compare the handwritten type with the generated Api type. Key differences:
readonly modifiers — generated types mark read-only fieldsrequired= preciselynull types are explicitSee type-compatibility.md for details.
See migration-patterns.md for detailed before/after examples covering:
api.surveys.get() → surveysRetrieve())api.get<T>(url) → generated function)Update downstream references from the handwritten type to the generated one:
// Before
function renderSurvey(survey: Survey): JSX.Element { ... }
// After
function renderSurvey(survey: SurveyApi): JSX.Element { ... }
After migrating all usages of a handwritten type:
~/types or the local filepnpm --filter=@posthog/frontend typescript:check to verify no breakage| Scenario | Action |
|---|---|
| Generated function exists | Replace manual call with generated function |
| Generated type exists but function doesn't | Use the generated type as the generic parameter on the manual call, file a follow-up to add @extend_schema |
| Neither exists | Keep the manual pattern, fix the backend serializer/viewset first |
| Custom action without generated equivalent | Keep the api.<entity>.<method>() call, fix the backend @action annotation first |
| Generated type has different shape than handwritten | Adapt call sites to the generated shape — the serializer is the source of truth |
| Code mutates the response object | Use a local mutable copy: const mutable = { ...response } and mutate that |
| Need both read and write types | Use FooApi for reads, derive write types via Parameters<typeof fooCreate>[1] or use PatchedFooApi |
// Core generated functions — import from api.ts
import { domainsList, domainsCreate, domainsRetrieve } from '~/generated/core/api'
// Core generated types — import type from api.schemas.ts
import type { OrganizationDomainApi } from '~/generated/core/api.schemas'
// Product generated functions — NO tilde prefix, use 'products/' path
import { surveysList, surveysRetrieve } from 'products/surveys/frontend/generated/api'
import type { SurveyApi } from 'products/surveys/frontend/generated/api.schemas'
// Within a product, relative imports also work
import { logsAlertsCreate } from '../generated/api'
import type { LogsAlertConfigurationApi } from '../generated/api.schemas'
Path rules:
~/generated/core/... (tilde prefix)products/<product>/frontend/generated/... (no tilde)../generated/... or ./generated/...Use import type for types to enable proper tree-shaking.
Generated functions wrap the same api module via api-orval-mutator.ts:
surveysList(projectId, params)
→ apiMutator(url, { method: 'GET' })
→ api.get(url)
Switching to generated functions does not change HTTP behavior — same cookies, same CSRF, same error handling. The only difference is type safety and URL construction.
pnpm --filter=@posthog/frontend typescript:checkhogli test <test_file>improving-drf-endpoints to fix serializers that produce poor typesdocs/published/handbook/engineering/type-system.mdfrontend/src/lib/api-orval-mutator.tshogli build:openapi