[Docs] Custom route Endpoints interface — full documentation with examples
Problem
The Endpoints interface pattern for typing custom Strapi controller routes is
undocumented. It is not mentioned anywhere in the official strapi-typed-client
docs. Developers who need custom routes have to:
- Read the generator source code to discover the interface exists
- Guess the supported shape by trial and error
- Hope the codegen picks up their controller file correctly
This is a high-friction experience for something that is used in virtually every
real Strapi project — custom business logic routes are the norm, not the exception.
What needs to be documented
The Endpoints interface
The generator scans Strapi controller files for an exported Endpoints interface.
Each key maps to a custom route method. The generator reads this interface and emits
typed methods on the collection's client class.
Basic structure:
// src/api/{collection}/controllers/{collection}.ts
export interface Endpoints {
methodName: {
// shape of ctx.request.body
body?: { ... };
// shape of ctx.request.query
query?: { ... };
// URL params (e.g. :documentId in the route path)
params?: { ... };
// shape of the response body (what ctx.body.data is set to)
response: { ... };
};
}
All fields except response are optional depending on the HTTP method and route.
Examples
Example 1 — Simple POST with body and response
The most common pattern: a POST action that takes a documentId and returns a
success/error envelope. Matches the connection controller pattern.
// controller
export interface Endpoints {
sync: {
body: {
documentId: string;
};
response: {
success: boolean;
errors?: Array<{ message: string }>;
};
};
}
Generated client method:
// Generated on ConnectionAPI
async sync(data: { documentId: string }): Promise<{
success: boolean;
errors?: Array<{ message: string }>;
}>;
Usage:
const result = await client.connections.sync({ documentId: "abc123" });
if (!result.success) {
result.errors?.forEach(e => console.error(e.message));
}
Example 2 — GET with URL params
A route that reads from the URL path, no body.
// Route: GET /api/reservations/invoice/:documentId
export interface Endpoints {
invoice: {
params: {
documentId: string;
};
response: {
success: boolean;
html: string | null;
filename: string | null;
error?: string;
};
};
}
Generated client method:
async invoice(documentId: string): Promise<{
success: boolean;
html: string | null;
filename: string | null;
error?: string;
}>;
Usage:
const invoice = await client.reservations.invoice(documentId);
if (invoice.success && invoice.html) {
// render invoice
}
Example 3 — GET with query parameters
A route that accepts filters or options as query string parameters.
// Route: GET /api/units/availability
export interface Endpoints {
availability: {
query: {
dateFrom: string; // ISO date string
dateTo: string;
locationId?: string;
};
response: Array<{
documentId: string;
title: string;
available: boolean;
}>;
};
}
Generated client method:
async availability(query: {
dateFrom: string;
dateTo: string;
locationId?: string;
}): Promise<Array<{ documentId: string; title: string; available: boolean }>>;
Usage:
const units = await client.units.availability({
dateFrom: "2024-06-01",
dateTo: "2024-06-07",
});
Example 4 — Response with a related entity
When the response includes a fully populated Strapi entity, reference the generated
entity type directly in the response shape.
import type { Reservation } from "../../../types"; // generated types
// Route: POST /api/reservations/confirm
export interface Endpoints {
confirm: {
body: {
documentId: string;
ownerNote?: string;
};
response: {
success: boolean;
reservation: Reservation; // full entity returned
};
};
}
Generated client method:
async confirm(data: {
documentId: string;
ownerNote?: string;
}): Promise<{
success: boolean;
reservation: Reservation;
}>;
Example 5 — Response with a media file
When the route returns an uploaded file (e.g. a generated PDF stored via the upload
plugin), use the MediaFile type from the generated base types.
import type { MediaFile } from "../../../types";
// Route: POST /api/reservations/generate-pdf
export interface Endpoints {
generatePdf: {
body: {
documentId: string;
locale?: string;
};
response: {
success: boolean;
file: MediaFile | null;
};
};
}
Generated client method:
async generatePdf(data: {
documentId: string;
locale?: string;
}): Promise<{
success: boolean;
file: MediaFile | null;
}>;
Example 6 — File upload (FormData body)
When the route accepts a file upload, mark the body as FormData. The generator
will type the method to accept FormData instead of a plain object.
// Route: POST /api/units/import
export interface Endpoints {
import: {
body: FormData;
response: {
imported: number;
skipped: number;
errors: Array<{ row: number; message: string }>;
};
};
}
Generated client method:
async import(data: FormData): Promise<{
imported: number;
skipped: number;
errors: Array<{ row: number; message: string }>;
}>;
Usage:
const form = new FormData();
form.append("file", fileBlob, "units.csv");
const result = await client.units.import(form);
console.log(`Imported: ${result.imported}, skipped: ${result.skipped}`);
Example 7 — Multiple methods on one controller
A single controller can expose multiple custom routes. All methods are declared
in the same Endpoints interface:
// Route: POST /api/connections/sync
// Route: POST /api/connections/sync-unit
// Route: POST /api/connections/request-sync
// Route: POST /api/connections/request-unit
export interface Endpoints {
sync: {
body: { documentId: string };
response: { success: boolean; errors?: Array<{ message: string }> };
};
syncUnit: {
body: { documentId: string };
response: { success: boolean; errors?: Array<{ message: string }> };
};
requestSync: {
body: { documentId: string };
response: { success: boolean; errors?: Array<{ message: string }> };
};
requestUnit: {
body: { documentId: string };
response: { success: boolean; errors?: Array<{ message: string }> };
};
}
Example 8 — Standalone controller (no collection)
For routes that don't belong to a content type (e.g. a custom plugin route or
a utility endpoint), the Endpoints interface is placed on a standalone controller.
The generator creates a separate API class on the client rather than extending a
CollectionAPI.
// src/api/ical/controllers/ical.ts
export interface Endpoints {
findOne: {
params: { documentId: string };
response: string; // plain iCal text
};
}
Generated client property:
// StrapiClient
ical: IcalAPI;
// IcalAPI
async findOne(documentId: string): Promise<string>;
How the generator resolves Endpoints
| Controller file location |
Generated client property |
src/api/{name}/controllers/{name}.ts |
client.{pluralName} — extends CollectionAPI |
src/api/{name}/controllers/{name}.ts (single type) |
client.{name} — extends SingleTypeAPI |
| Any controller without a matching content type |
Standalone class — separate property on StrapiClient |
The generator matches the controller file to a schema by comparing the content
type UID (api::{name}) declared in factories.createCoreController(...). If no
matching schema exists, a standalone API class is created.
Namespace types for complex responses
For controllers with complex or reused response shapes, the recommended pattern
is to export a TypeScript namespace alongside the Endpoints interface. The
generator picks this up and exposes it as a typed namespace on the client.
// Recommended for complex response types
export namespace ConnectionAPI {
export type SyncResponse = {
success: boolean;
errors?: Array<{ message: string }>;
processedAt?: string;
};
}
export interface Endpoints {
sync: {
body: { documentId: string };
response: ConnectionAPI.SyncResponse; // reference the namespace type
};
syncUnit: {
body: { documentId: string };
response: ConnectionAPI.SyncResponse; // reuse across methods
};
}
Generated namespace on the client:
// Consumer code
import type { ConnectionAPI } from "@org/shared-types";
const result: ConnectionAPI.SyncResponse = await client.connections.sync({
documentId: "abc123",
});
Strapi controller side — recommended structure
For reference, the full recommended controller file structure that the generator
expects:
import { factories } from "@strapi/strapi";
/**
* Typed endpoint definitions for strapi-typed-client codegen.
* Re-run `generate-types` after changing this interface.
*/
export interface Endpoints {
myAction: {
body: { documentId: string };
response: { success: boolean };
};
}
export default factories.createCoreController("api::{plugin}.{name}", ({ strapi }) => ({
myAction: async (ctx) => {
const { documentId } = ctx.request.body;
if (!documentId) {
ctx.status = 400;
ctx.body = { data: { success: false, errors: [{ message: "documentId is required" }] } };
return;
}
const result = await strapi.service("api::{plugin}.{name}").myAction(documentId);
ctx.status = result.success ? 200 : 400;
ctx.body = { data: result };
},
}));
Key points:
- Always wrap the response in
{ data: result } — the client unwraps .data automatically
Endpoints must be a named export interface, not a type alias
- The interface must be in the same file as the controller default export
- Re-run
generate-types after any change to Endpoints
Summary
| Scenario |
body |
query |
params |
response |
| POST with documentId |
{ documentId: string } |
— |
— |
{ success: boolean } |
| GET with URL segment |
— |
— |
{ documentId: string } |
entity or shape |
| GET with query string |
— |
{ field: type } |
— |
array or shape |
| Related entity in response |
— |
— |
— |
import entity type |
| Media file in response |
— |
— |
— |
MediaFile |
| File upload |
FormData |
— |
— |
result shape |
| Multiple methods |
separate key per method in same interface |
|
|
|
| Standalone (no collection) |
same as above |
|
|
standalone class generated |
[Docs] Custom route
Endpointsinterface — full documentation with examplesProblem
The
Endpointsinterface pattern for typing custom Strapi controller routes isundocumented. It is not mentioned anywhere in the official
strapi-typed-clientdocs. Developers who need custom routes have to:
This is a high-friction experience for something that is used in virtually every
real Strapi project — custom business logic routes are the norm, not the exception.
What needs to be documented
The
EndpointsinterfaceThe generator scans Strapi controller files for an exported
Endpointsinterface.Each key maps to a custom route method. The generator reads this interface and emits
typed methods on the collection's client class.
Basic structure:
All fields except
responseare optional depending on the HTTP method and route.Examples
Example 1 — Simple POST with body and response
The most common pattern: a POST action that takes a
documentIdand returns asuccess/error envelope. Matches the connection controller pattern.
Generated client method:
Usage:
Example 2 — GET with URL params
A route that reads from the URL path, no body.
Generated client method:
Usage:
Example 3 — GET with query parameters
A route that accepts filters or options as query string parameters.
Generated client method:
Usage:
Example 4 — Response with a related entity
When the response includes a fully populated Strapi entity, reference the generated
entity type directly in the
responseshape.Generated client method:
Example 5 — Response with a media file
When the route returns an uploaded file (e.g. a generated PDF stored via the upload
plugin), use the
MediaFiletype from the generated base types.Generated client method:
Example 6 — File upload (FormData body)
When the route accepts a file upload, mark the body as
FormData. The generatorwill type the method to accept
FormDatainstead of a plain object.Generated client method:
Usage:
Example 7 — Multiple methods on one controller
A single controller can expose multiple custom routes. All methods are declared
in the same
Endpointsinterface:Example 8 — Standalone controller (no collection)
For routes that don't belong to a content type (e.g. a custom plugin route or
a utility endpoint), the
Endpointsinterface is placed on a standalone controller.The generator creates a separate API class on the client rather than extending a
CollectionAPI.Generated client property:
How the generator resolves
Endpointssrc/api/{name}/controllers/{name}.tsclient.{pluralName}— extendsCollectionAPIsrc/api/{name}/controllers/{name}.ts(single type)client.{name}— extendsSingleTypeAPIStrapiClientThe generator matches the controller file to a schema by comparing the content
type UID (
api::{name}) declared infactories.createCoreController(...). If nomatching schema exists, a standalone API class is created.
Namespace types for complex responses
For controllers with complex or reused response shapes, the recommended pattern
is to export a TypeScript namespace alongside the
Endpointsinterface. Thegenerator picks this up and exposes it as a typed namespace on the client.
Generated namespace on the client:
Strapi controller side — recommended structure
For reference, the full recommended controller file structure that the generator
expects:
Key points:
{ data: result }— the client unwraps.dataautomaticallyEndpointsmust be a namedexport interface, not a type aliasgenerate-typesafter any change toEndpointsSummary
bodyqueryparamsresponse{ documentId: string }{ success: boolean }{ documentId: string }{ field: type }MediaFileFormData