Type Helpers
Extract and use types from your OpenAPI schema.
You need a type for a form field or a hook return value, and you're about to write interface User by hand. Stop.
Type helpers extract request bodies, response types, and schema models from your OpenAPI spec. But first — check whether a named contract already exists. It usually does.
Prefer api.contracts.ts for day-to-day types. Named interfaces like UserDto, CreateUserBody, and ListUsersQueryParams are generated for every schema and operation — import those first. Use the helpers below when you need path/operation-based extraction or no named contract exists yet.
Two Approaches
Two ways to reference an endpoint when extracting types. Same output — pick what matches how you think about the API.
Path-Based
Reference the URL path template and HTTP method — exactly as in your OpenAPI spec. Natural if you think "I'm calling GET /users/{id}."
Helpers: ApiRequestBody, ApiResponseData, ApiPathParams, ApiQueryParams, ApiStatusCodes
Operation-Based
Reference the operationId from your spec. Cleaner when operations are well-named (getUserById, createOrder).
Helpers: ServerRequestBody, ServerRequestParams, ServerResponseType
Not sure which to use? Check api.contracts.ts first. If UserDto exists, import it. Helpers are for when it doesn't — or when you're extracting by path/method and don't want to grep the contracts file.
Extract Request Body
type CreateUserInput = ApiRequestBody<"/users", "post">;
// { name: string; email: string }type CreateUserInput = ServerRequestBody<"createUser">;
// { name: string; email: string }If CreateUserBody exists in contracts, just import it. These helpers are the fallback.
Extract Response Type
type User = ApiResponseData<"/users/{id}", "get">;
// With specific status code
type CreatedUser = ApiResponseData<"/users", "post", 201>;type User = ServerResponseType<"getUserById">;
// With specific status code
type NotFound = ServerResponseType<"getUserById", 404>;Extract Parameters
Path Parameters
type UserParams = ApiPathParams<"/users/{id}">;
// { id: string | number }Query Parameters
type ListQuery = ApiQueryParams<"/users", "get">;
// { limit?: number; offset?: number }type Params = ServerRequestParams<"getUserById">;
// { path: { id: string }; query?: { include?: string[] } }Combines path and query params in one type.
Extract Status Codes
type UserStatusCodes = ApiStatusCodes<"/users/{id}", "get">;
// 200 | 404 | 500Schema Models
Preferred — import from contracts:
import type {
UserDto,
PostDto,
PaginatedUserResponseDto,
} from "@/services/api/_generated/api.contracts";
type User = UserDto;
type Post = PostDto;
type PaginatedUsers = PaginatedUserResponseDto;This is what The Query Layer uses. Named interfaces, cmd+click navigation, no string keys to misremember.
ServerModel still works for a string-keyed lookup into components.schemas, but you're choosing the scenic route:
type User = ServerModel<"User">;
type Post = ServerModel<"Post">;
type PaginatedUsers = ServerModel<"PaginatedUserResponse">;If both exist, pick contracts. Your future self grepping for UserDto will thank you.
Utility Helpers
Paths & Methods
type AllPaths = Paths;
// "/users" | "/users/{id}" | "/posts" | ...
type Method = HttpMethod;
// "get" | "post" | "put" | "delete" | "patch"Better IDE Tooltips
// Expand shows full structure instead of type reference
type ExpandedUser = Expand<User & { posts: Post[] }>;
// ExpandRecursively goes through nested types
type DeepUser = ExpandRecursively<UserWithRelations>;Examples
import type { CreateUserBody } from "@/services/api/_generated/api.contracts";
const [form, setForm] = useState<CreateUserBody>({
name: "",
email: "",
});import type { UserDto } from "@/services/api/_generated/api.contracts";
function useUser(id: string) {
const [user, setUser] = useState<UserDto | null>(null);
// ...
}import type {
CreateUserBody,
UserDto,
} from "@/services/api/_generated/api.contracts";
export const userService = {
create: (input: CreateUserBody) => api.op.createUser(input),
getById: (id: string) => api.op.getUserById({ id }),
};
type User = UserDto;In a real app, this service layer lives inside a query class — see The Query Layer.