--- url: /strapi-typed-client/guide/getting-started.md --- # Getting Started This guide walks you through installing `strapi-typed-client`, registering the Strapi plugin, generating types, and making your first typed API call. ## Requirements * **Strapi v5** (Strapi v4 is not supported) * **Node.js >= 22** ## Installation Install the package in both your Strapi backend and your frontend project: ::: code-group ```bash [npm] npm install strapi-typed-client ``` ```bash [yarn] yarn add strapi-typed-client ``` ```bash [pnpm] pnpm add strapi-typed-client ``` ::: ## Register the Strapi Plugin The package includes a Strapi plugin that exposes your schema via a REST endpoint. Since the `strapi` field in `package.json` declares it as a plugin, Strapi auto-discovers it — no manual `import` or `resolve` needed. Enable it in your Strapi project's `config/plugins.ts`: ```ts // config/plugins.ts export default { 'strapi-typed-client': { enabled: true, config: { requireAuth: false, // default: false in dev, true in production }, }, } ``` After adding the config, **restart your Strapi server**. ::: warning When `requireAuth` is not set, the plugin defaults to requiring auth in production (`NODE_ENV === 'production'`) and not requiring it in development. Set it explicitly if you need a specific behavior. ::: ### Plugin Endpoints Once the plugin is active, two endpoints become available: | Endpoint | Description | | ------------------------------------------ | -------------------------------------------------------- | | `GET /api/strapi-typed-client/schema` | Returns the full content-type schema as JSON + hash | | `GET /api/strapi-typed-client/schema-hash` | Returns only the schema hash (used for change detection) | ::: tip These endpoints are used by the CLI to fetch your schema. You do not need to call them manually. ::: ### Creating an API Token If you enable `requireAuth: true` (or deploy to production where it's enabled by default), you need an API token. To create one: 1. Open the Strapi admin panel. 2. Go to **Settings** → **Global Settings** → **API Tokens**. 3. Click **Create new API Token**. 4. Set a **Name** (e.g., "Types Generator"), choose **Token type** — `Read-only` is sufficient for schema fetching. 5. Click **Save** and copy the token immediately — it is shown only once. Pass the token to the CLI: ```bash npx strapi-types generate --url http://localhost:1337 --token YOUR_TOKEN ``` Or via environment variable: ```bash STRAPI_TOKEN=your-token npx strapi-types generate ``` See [Authentication](/advanced/authentication) for more details. ## Generate Types With Strapi running, generate the types and a typed client into your project, and commit the result: ```bash npx strapi-types generate --url http://localhost:1337 --output ./src/strapi ``` This writes three files into `./src/strapi`: | File | Description | | ---------- | -------------------------------------------------------------- | | `types.*` | TypeScript interfaces for all content types and components | | `client.*` | A typed `StrapiClient` class with methods for every collection | | `index.*` | Re-exports everything from types and client | Add a path alias once so you can import them from anywhere in your app: ```json // tsconfig.json { "compilerOptions": { "paths": { "@/*": ["./src/*"] } } } ``` ```ts import { StrapiClient } from '@/strapi' ``` Committing the generated code keeps it through reinstalls and surfaces schema changes as reviewable diffs. The client is self-contained, so your app gains no runtime dependency on `strapi-typed-client`. `--format` defaults to compiled `.js` + `.d.ts` (no build step required). Pass `--format ts` to emit raw `.ts` for bundlers and monorepos that compile their own sources. Either way, `--output` is required and must point at your source tree. ::: warning Regenerate after upgrading The generated client records the `strapi-typed-client` version that produced it. After upgrading the package, run `generate` again so committed types pick up generator fixes — `strapi-types check` warns when they drift. ::: ## Quick Usage Once types are generated, create a client and start making typed API calls: ```ts import { StrapiClient } from '@/strapi' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', }) // Fully typed — autocomplete for filters, sort, populate const articles = await strapi.articles.find({ filters: { title: { $contains: 'hello' } }, sort: ['createdAt:desc'], pagination: { page: 1, pageSize: 10 }, }) console.log(articles) // Article[] ``` ::: info Import paths in these docs Examples import from `@/strapi` — the directory you generated into. ::: ::: info The client uses the global `fetch` by default. In Next.js, this means you automatically get all of Next.js's fetch optimizations (caching, deduplication, revalidation). See the [Next.js integration guide](/guide/nextjs) for details. ::: ## Next Steps * [CLI Commands Reference](/guide/cli) — all CLI options and environment variables * [Client Usage](/guide/client) — CRUD operations, auth, error handling * [Populate & Type Inference](/guide/populate) — nested populate with automatic type narrowing * [Next.js Integration](/guide/nextjs) — auto-generation, cache options, ISR --- --- url: /strapi-typed-client/guide/cli.md --- # CLI Reference The `strapi-types` CLI fetches your Strapi schema and generates TypeScript types and a typed client. ## Commands ### `generate` Connects to your Strapi instance, fetches the schema, and generates TypeScript output files. ```bash npx strapi-types generate --url http://localhost:1337 ``` **Options:** | Option | Description | Default | | ---------- | ------------------------------------------------------------------ | ------------------------------------------------ | | `--url` | Strapi server URL | `STRAPI_URL` env var | | `--token` | API token for authenticated access | `STRAPI_TOKEN` env var | | `--output` | Output directory for generated files | required (your source tree, e.g. `./src/strapi`) | | `--silent` | Suppress all console output | `false` | | `--force` | Regenerate even if schema has not changed | `false` | | `--format` | Output format: `js` (compiled `.js` + `.d.ts`) or `ts` (raw `.ts`) | `js` | **Examples:** ```bash # Generate into your source tree and commit it npx strapi-types generate --url http://localhost:1337 --output ./src/strapi # With authentication npx strapi-types generate --url http://localhost:1337 --token abc123 --output ./src/strapi # Force regeneration (ignore schema hash) npx strapi-types generate --url http://localhost:1337 --output ./src/strapi --force # Emit raw TypeScript instead of compiled .js + .d.ts npx strapi-types generate --output ./src/strapi --format ts ``` ::: warning `--output` is required `--output` must point at a directory in your source tree (e.g. `./src/strapi`). Generate the client there and commit the result so your types are durable and reviewable. You then import from that directory (e.g. `'@/strapi'`). ::: ::: tip `--format js` vs `--format ts` `js` (default) emits compiled `.js` + `.d.ts` — runs anywhere, including plain JavaScript projects with no build step. `ts` emits raw `.ts` for bundlers and monorepos that compile their own sources (Turborepo, Nx, pnpm workspaces); your `tsconfig.json` needs `moduleResolution: "bundler"` or `"nodenext"` so the `.js`-extension imports resolve to `.ts` source. Both are written into your source tree and committed. ::: ### `check` Compares the schema hash baked into your generated client against the live Strapi schema, and reports whether your types are up to date. Useful in CI. ```bash npx strapi-types check --url http://localhost:1337 --output ./src/strapi ``` It will: 1. Exit `1` with a clear message if no generated client is found (run `generate` first) 2. Warn if the generated client was produced by a different `strapi-typed-client` version than the one installed 3. Fetch the remote schema hash and compare it to the local one 4. Exit `0` when in sync, `1` when out of sync Pass `--output` to point at the directory you generated into. It is required. ### `watch` Connects to the Strapi SSE stream and automatically regenerates types when the schema changes. ```bash npx strapi-types watch --url http://localhost:1337 ``` This is useful during development. The command runs continuously and: 1. Opens an SSE connection to `/api/strapi-typed-client/schema-watch` 2. Receives the current schema hash on connect 3. Regenerates types only when the hash differs from the local one 4. Automatically reconnects if the Strapi server restarts ::: tip For Next.js projects, consider using the `withStrapiTypes` wrapper instead of running `watch` manually. See the [Next.js guide](/guide/nextjs). ::: ## Environment Variables Instead of passing flags on every invocation, you can set environment variables: | Variable | Equivalent Flag | | -------------- | --------------- | | `STRAPI_URL` | `--url` | | `STRAPI_TOKEN` | `--token` | **Example with a `.env` file:** ```bash # .env STRAPI_URL=http://localhost:1337 STRAPI_TOKEN=your-api-token-here ``` ```bash # Now you can run without flags npx strapi-types generate ``` ::: warning CLI flags take precedence over environment variables. If both are provided, the flag value is used. ::: ## Schema Hashing The CLI uses schema hashing to avoid unnecessary regeneration. When you run `generate`: 1. The CLI fetches the schema hash from `/api/strapi-typed-client/schema-hash` 2. It compares the hash against the previously stored hash 3. If the hashes match, generation is skipped (unless `--force` is used) 4. If the hashes differ, types are regenerated and the new hash is stored This makes it safe to run `generate` in CI or on every build without wasting time on unchanged schemas. ```bash # First run — generates types npx strapi-types generate --url http://localhost:1337 --output ./src/strapi # Second run — skipped, schema unchanged npx strapi-types generate --url http://localhost:1337 --output ./src/strapi # Force regeneration regardless of hash npx strapi-types generate --url http://localhost:1337 --output ./src/strapi --force ``` ## Usage in package.json Scripts A typical setup in your frontend project: ```json { "scripts": { "generate-types": "strapi-types generate --output ./src/strapi", "check-strapi": "strapi-types check --output ./src/strapi", "dev": "strapi-types watch --output ./src/strapi & next dev", "build": "strapi-types generate --output ./src/strapi --force && next build" } } ``` --- --- url: /strapi-typed-client/guide/client.md --- # StrapiClient Usage The generated `StrapiClient` class provides typed methods for every collection in your Strapi instance. This page covers how to create a client, perform CRUD operations, handle authentication, and deal with errors. ## Creating a Client ```ts import { StrapiClient } from '@/strapi' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', }) ``` ### With Authentication ```ts const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', token: 'your-bearer-token', }) ``` ### With a Custom Fetch Function The client uses the global `fetch` by default. You can provide a custom implementation for environments where global fetch is not available or when you need special behavior: ```ts import nodeFetch from 'node-fetch' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', fetch: nodeFetch as any, }) ``` ## Collection API Methods Every collection type in your Strapi schema gets its own property on the client with the following methods: | Method | Description | | ------------------------------ | ----------------------------------- | | `find(params?)` | Fetch a list of entries | | `findOne(documentId, params?)` | Fetch a single entry by document ID | | `create(data)` | Create a new entry | | `update(documentId, data)` | Update an existing entry | | `delete(documentId)` | Delete an entry | ## CRUD Operations ### find() Retrieve a list of entries with optional filtering, sorting, pagination, and population: ```ts // All articles const result = await strapi.articles.find() // With parameters const result = await strapi.articles.find({ filters: { published: { $eq: true } }, sort: ['createdAt:desc'], pagination: { page: 1, pageSize: 25 }, populate: { category: true }, }) // result is Article[] // use findWithMeta() if you need pagination metadata ``` ### findOne() Retrieve a single entry by its document ID: ```ts const result = await strapi.articles.findOne('abc123') // With populate const result = await strapi.articles.findOne('abc123', { populate: { category: true, author: true }, }) // result is Article | null ``` ### create() Create a new entry. The data parameter uses the generated input type with all writable fields: ```ts const result = await strapi.articles.create({ title: 'My New Article', content: 'Article body text...', category: 1, // relation as ID }) // result is Article ``` ### update() Update an existing entry. All fields are optional for partial updates: ```ts const result = await strapi.articles.update('abc123', { title: 'Updated Title', }) // result is Article ``` ### delete() Delete an entry by its document ID: ```ts const result = await strapi.articles.delete('abc123') ``` ## Response Format Strapi wraps responses in a `{ data, meta }` envelope over the wire, but the client **unwraps it for you** — each method resolves directly to the data: ```ts find() // Article[] findOne() // Article | null create() // Article update() // Article delete() // Article | null ``` When you need pagination metadata, use `findWithMeta()`, which returns the full envelope: ```ts const { data, meta } = await strapi.articles.findWithMeta() // data: Article[] // meta.pagination: { page, pageSize, pageCount, total } ``` ## Authentication ### Setting a Token at Creation ```ts const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', token: 'your-api-token', }) ``` ### Updating the Token at Runtime Use `setToken()` to change the authorization token after the client has been created. This is useful for login flows: ```ts const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', }) // After user logs in strapi.setToken(jwt) // All subsequent requests include the Authorization header const profile = await strapi.users.findOne(userId) ``` ::: tip The token is sent as a `Bearer` token in the `Authorization` header on every request. ::: ## Error Handling The client throws two types of errors: * **`StrapiError`** — the server responded with a non-OK HTTP status (400, 401, 404, 500, etc.) * **`StrapiConnectionError`** — the request never reached the server (network down, DNS failure, timeout) ```ts import { StrapiError, StrapiConnectionError } from '@/strapi' try { const result = await strapi.articles.findOne('nonexistent-id') } catch (error) { if (error instanceof StrapiConnectionError) { // Network-level failure — server unreachable, DNS error, timeout console.error('Cannot reach Strapi:', error.message) } else if (error instanceof StrapiError) { // Server responded with an error console.error(`HTTP ${error.status}:`, error.userMessage) } } ``` Error messages include contextual hints for common HTTP codes (401, 403, 404, 500), so even raw `error.message` is informative. ### Request Timeout Set the `timeout` option (in milliseconds) to abort requests that take too long: ```ts const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', timeout: 5000, // 5 seconds }) ``` When a request exceeds the timeout, a `StrapiConnectionError` is thrown with the message `Request timed out after 5000ms`. ### Common Pattern A helper that standardizes error handling across your application: ```ts async function safeFind(fn: () => Promise): Promise { try { return await fn() } catch (error) { console.error('Strapi request failed:', error) return null } } const articles = await safeFind(() => strapi.articles.find()) ``` ::: warning Always handle errors in production code. Network failures, authentication issues, and validation errors from Strapi will all result in thrown exceptions. ::: ## Single Types Single types (like a homepage or global settings) are accessed the same way as collections, but you typically only use `find()` and `update()`: ```ts // Fetch the homepage single type const homepage = await strapi.homepage.find({ populate: { hero: true, seo: true }, }) // Update it (single types take no documentId) await strapi.homepage.update({ title: 'Welcome' }) ``` --- --- url: /strapi-typed-client/guide/populate.md --- # Populate & Type Inference One of the most powerful features of `strapi-typed-client` is automatic type inference based on your `populate` parameter. The TypeScript return type changes depending on which relations you populate. ## Without Populate When you call `find()` or `findOne()` without a populate parameter, relations are returned as minimal reference objects: ```ts const result = await strapi.articles.find() // result[0] has: // { // id: number // documentId: string // title: string // content: string // category: { id: number; documentId: string } | null // tags: { id: number; documentId: string }[] // createdAt: string // updatedAt: string // } ``` Relations only include `id` and `documentId` by default. To get the full related object, you need to populate. ## With Populate ### Boolean Populate Pass `true` for any relation to include its full data: ```ts const result = await strapi.articles.find({ populate: { category: true, }, }) // Now category is fully typed: // result[0].category is: // { // id: number // documentId: string // name: string // slug: string // ... // } | null ``` TypeScript knows the exact shape of the populated relation. You get full autocomplete on `category.name`, `category.slug`, etc. ### Nested Populate Populate relations of relations by passing a nested object: ```ts const result = await strapi.articles.find({ populate: { category: { populate: { parentCategory: true, }, }, }, }) // result[0].category.parentCategory is now fully typed ``` You can nest as deeply as your schema requires: ```ts const result = await strapi.articles.find({ populate: { author: { populate: { avatar: true, organization: { populate: { logo: true, }, }, }, }, }, }) ``` ## How Type Inference Works The generated types include conditional type definitions that map populate parameters to return types. When you write: ```ts const result = await strapi.articles.find({ populate: { category: true }, }) ``` TypeScript evaluates the populate parameter at compile time and produces a return type where `category` is the full `Category` interface instead of just `{ id: number; documentId: string }`. ::: info This means you get compile-time errors if you try to access a field on an unpopulated relation: ```ts const result = await strapi.articles.find() // Type error: Property 'name' does not exist result[0].category.name // [!code error] ``` ::: ## Payload Types For advanced use cases, you can use the generated payload types directly to describe the shape of a response with specific populate options: ```ts import type { ArticleGetPayload } from '@/strapi' type ArticleWithCategory = ArticleGetPayload<{ populate: { category: true } }> // Use it as a function parameter type function renderArticle(article: ArticleWithCategory) { console.log(article.title) console.log(article.category.name) // fully typed } ``` This is especially useful when you need to pass fetched data between functions and want to preserve the populated type information. ::: warning Use `as const` when extracting populate to a variable If you define your populate object outside the method call, you **must** use `as const`. Without it, TypeScript widens `true` to `boolean` and `GetPayload` cannot infer the populated fields: ```ts // ❌ Type inference broken — `true` widens to `boolean` const populate = { category: true, tags: true } type Result = ArticleGetPayload<{ populate: typeof populate }> // Result has no populated fields // ✅ Correct — `as const` preserves literal types const populate = { category: true, tags: true } as const type Result = ArticleGetPayload<{ populate: typeof populate }> // Result includes full Category and Tag[] fields ``` This also applies when passing populate to client methods via a variable: ```ts const POPULATE = { category: true, author: true } as const // Type inference works correctly const result = await strapi.articles.find({ populate: POPULATE }) result[0].category.name // ✅ fully typed ``` When you pass the object inline, `as const` is not needed — TypeScript infers literal types automatically. ::: ## Populating Media Media fields work the same way: ```ts const result = await strapi.articles.find({ populate: { coverImage: true, }, }) // result[0].coverImage is: // { // id: number // name: string // url: string // width: number | null // height: number | null // formats: Record | null // ... // } | null ``` ## Populating Components Components that are part of a content type can also be populated: ```ts const result = await strapi.articles.find({ populate: { seo: true, // component field }, }) // result[0].seo is the full component type // { metaTitle: string, metaDescription: string, ... } ``` ::: tip Populate only what you need. Each populated relation adds to the response size and query time. The type system helps you here — you only get typed access to fields you explicitly populate. ::: --- --- url: /strapi-typed-client/guide/filtering.md --- # Filtering, Sorting & Pagination The `find()` method accepts a params object with `filters`, `sort`, `pagination`, and `fields` options. All filter types are generated per-entity, giving you full autocomplete and type safety. ## Filters ### Comparison Operators | Operator | Description | Example | | ------------- | --------------------- | ------------------------------------------------ | | `$eq` | Equal | `{ title: { $eq: 'Hello' } }` | | `$ne` | Not equal | `{ status: { $ne: 'draft' } }` | | `$in` | In array | `{ status: { $in: ['published', 'archived'] } }` | | `$lt` | Less than | `{ price: { $lt: 100 } }` | | `$lte` | Less than or equal | `{ price: { $lte: 100 } }` | | `$gt` | Greater than | `{ price: { $gt: 0 } }` | | `$gte` | Greater than or equal | `{ price: { $gte: 10 } }` | | `$contains` | Contains substring | `{ title: { $contains: 'strapi' } }` | | `$startsWith` | Starts with | `{ slug: { $startsWith: 'blog-' } }` | | `$endsWith` | Ends with | `{ email: { $endsWith: '@company.com' } }` | | `$null` | Is null | `{ deletedAt: { $null: true } }` | | `$notNull` | Is not null | `{ publishedAt: { $notNull: true } }` | **Example:** ```ts const result = await strapi.articles.find({ filters: { title: { $contains: 'typescript' }, views: { $gte: 100 }, status: { $eq: 'published' }, }, }) ``` ### Logical Operators Combine multiple conditions with `$and`, `$or`, and `$not`: ```ts // $or — match any condition const result = await strapi.articles.find({ filters: { $or: [ { title: { $contains: 'strapi' } }, { title: { $contains: 'nextjs' } }, ], }, }) // $and — match all conditions (implicit when listing multiple fields) const result = await strapi.articles.find({ filters: { $and: [{ status: { $eq: 'published' } }, { views: { $gt: 50 } }], }, }) // $not — negate a condition const result = await strapi.articles.find({ filters: { $not: { status: { $eq: 'draft' }, }, }, }) ``` ### Nested Field Filtering Filter on fields of related entities: ```ts const result = await strapi.articles.find({ filters: { category: { slug: { $eq: 'technology' }, }, author: { name: { $contains: 'John' }, }, }, }) ``` ::: tip Nested filters work on relations without needing to populate them. Strapi resolves the join on the server side. ::: ## Sorting Pass an array of sort strings in the format `field:direction`: ```ts const result = await strapi.articles.find({ sort: ['createdAt:desc'], }) // Multiple sort fields const result = await strapi.articles.find({ sort: ['featured:desc', 'title:asc'], }) ``` Valid directions are `asc` (ascending) and `desc` (descending). If no direction is specified, `asc` is used by default. ## Pagination Control the page number and page size: ```ts const result = await strapi.articles.find({ pagination: { page: 1, pageSize: 10, }, }) // Pagination metadata is in the response console.log(result.meta.pagination) // { // page: 1, // pageSize: 10, // pageCount: 5, // total: 42, // } ``` ::: warning The default page size in Strapi is 25. The maximum page size is 100 unless configured otherwise in your Strapi settings. ::: ## Fields Selection Select only specific fields to reduce response size: ```ts const result = await strapi.articles.find({ fields: ['title', 'slug', 'createdAt'], }) // Only the selected fields are returned ``` ## Combining Parameters All parameters can be used together: ```ts const result = await strapi.articles.find({ filters: { status: { $eq: 'published' }, category: { slug: { $in: ['tech', 'science'] }, }, }, sort: ['publishedAt:desc'], pagination: { page: 1, pageSize: 12, }, populate: { category: true, author: true, }, fields: ['title', 'slug', 'excerpt', 'publishedAt'], }) ``` ## Entity-Specific Filter Types The generated types include filter type definitions for each entity. This means you get autocomplete for field names and type checking for filter values: ```ts // TypeScript will error if you filter on a non-existent field const result = await strapi.articles.find({ filters: { nonExistentField: { $eq: 'value' }, // [!code error] }, }) // TypeScript will error if the value type doesn't match the field const result = await strapi.articles.find({ filters: { views: { $eq: 'not a number' }, // [!code error] }, }) ``` ::: info Filter types are generated based on your Strapi schema. When your schema changes and you regenerate types, the filter types update automatically. ::: --- --- url: /strapi-typed-client/guide/input-types.md --- # Input Types `strapi-typed-client` generates separate input types for create and update operations. These types reflect what Strapi expects when writing data, which differs from what it returns when reading. ## Base Type vs Input Type When you read data from Strapi, you get the full entity with all computed and populated fields: ```ts // Reading — base type (Article) { id: number documentId: string title: string content: string category: { id: number; documentId: string } | null tags: { id: number; documentId: string }[] coverImage: { id: number; url: string; ... } | null createdAt: string updatedAt: string } ``` When you write data (create or update), you use the input type where relations and media are referenced by ID: ```ts // Writing — input type (ArticleInput) { title?: string | null content?: string | null category?: RelationInput // relation — id, documentId, array, or { connect | disconnect | set } tags?: RelationInput // relation (any cardinality) coverImage?: MediaInput // media — file id } ``` ## Relations In input types, every relation — regardless of cardinality — is typed as `RelationInput`: ```ts type StrapiID = string | number type RelationInput = | StrapiID // a single id or documentId | StrapiID[] // an array of ids | { connect?: StrapiID[]; disconnect?: StrapiID[]; set?: StrapiID[] } // explicit relation operations | null ``` | Relation Type | Input Type | | ------------- | --------------- | | One-to-one | `RelationInput` | | Many-to-one | `RelationInput` | | One-to-many | `RelationInput` | | Many-to-many | `RelationInput` | A plain id or array is shorthand for `set` — it overwrites the existing relations. Use the explicit `{ connect | disconnect | set }` form for fine-grained updates. ```ts await strapi.articles.create({ title: 'New Article', category: 5, // link to category with id 5 tags: [1, 3, 7], // set tags to ids 1, 3, 7 }) // Fine-grained update without overwriting the whole list await strapi.articles.update('abc123', { tags: { connect: [9], disconnect: [3] }, }) ``` ## Media Single-media fields are typed `MediaInput` (`StrapiID | null`); multi-media fields are `MultiMediaInput` (`StrapiID[] | null`). Both reference an already-uploaded file by its numeric id: ```ts await strapi.articles.create({ title: 'New Article', coverImage: 12, // single media — file id 12 gallery: [12, 15, 20], // multi media — array of file ids }) ``` ::: info File uploads are handled separately through Strapi's upload API. The input type only accepts the id of an existing media entry. ::: ## Components as Objects Component fields in input types accept plain objects matching the component's input shape: ```ts await strapi.articles.create({ title: 'New Article', seo: { metaTitle: 'Article about TypeScript', metaDescription: 'A deep dive into type safety.', keywords: 'typescript, strapi, types', }, }) ``` Repeatable components accept an array of objects: ```ts await strapi.articles.create({ title: 'New Article', sections: [ { heading: 'Introduction', body: '...' }, { heading: 'Conclusion', body: '...' }, ], }) ``` ## Partial Updates For `update()`, all fields in the input type are optional. This allows partial updates where you only send the fields that changed: ```ts // Only update the title — all other fields remain unchanged await strapi.articles.update('abc123', { title: 'Updated Title', }) // Clear a relation by setting it to null await strapi.articles.update('abc123', { category: null, }) // Replace all tags await strapi.articles.update('abc123', { tags: [2, 4, 6], }) ``` ## Nullable Fields Fields that are not required in your Strapi schema accept `null` in the input type: ```ts await strapi.articles.create({ title: 'Article', // required — cannot be null subtitle: null, // optional — can be null category: null, // relation — can be null coverImage: null, // media — can be null }) ``` ## Full Create Example Here is a complete example creating an article with all field types: ```ts const result = await strapi.articles.create({ // Scalar fields title: 'Getting Started with Strapi v5', slug: 'getting-started-strapi-v5', content: 'Full article content here...', views: 0, featured: true, publishedAt: '2025-01-15T10:00:00.000Z', // Relations (as IDs) category: 3, tags: [1, 5, 12], author: 7, // Media (as ID) coverImage: 42, // Component seo: { metaTitle: 'Getting Started with Strapi v5', metaDescription: 'Learn how to use Strapi v5 with TypeScript.', }, // Repeatable component sections: [ { heading: 'Introduction', body: 'Welcome...' }, { heading: 'Setup', body: 'First, install...' }, ], }) ``` ::: tip The generated input types give you full autocomplete, so you do not need to memorize field names or types. Your editor will show you exactly what fields are available and what types they expect. ::: ## Validation Constraints Constraints declared in your Strapi schema (`min`, `max`, `minLength`, `maxLength`, `regex`, `default`) are surfaced as JSDoc tags on both the base type and the input type. Your editor shows them on hover and in autocomplete, so the rules live next to the field: ```ts export interface ArticleInput { /** * @minLength 3 * @maxLength 120 */ title?: string | null /** @pattern ^[a-z0-9-]+$ */ slug?: string | null /** * @minimum 0 * @maximum 100 */ discount?: number | null /** @default "draft" */ status?: 'draft' | 'published' | null } ``` The tags follow the [ts-to-zod](https://github.com/fabien0102/ts-to-zod) / TypeDoc convention, so downstream tooling can read them too: | Strapi schema | JSDoc tag | | ------------- | ------------ | | `min` | `@minimum` | | `max` | `@maximum` | | `minLength` | `@minLength` | | `maxLength` | `@maxLength` | | `regex` | `@pattern` | | `default` | `@default` | ::: info These tags are informational — they document the schema in your editor and for tooling. The client does not enforce them at runtime yet; generated runtime validators (Zod) are planned in a later phase. ::: ## Default Values For every content type and component that declares schema defaults, a `*Defaults` constant is generated. Each holds only the fields that have a `default` in the schema, typed `as const satisfies Partial<*Input>`: ```ts export const ArticleDefaults = { status: 'draft', featured: false, views: 0, } as const satisfies Partial ``` Use it as a single source of truth for form seeds — the same defaults the server would apply: ```ts import { ArticleDefaults } from './strapi/types' // Seed a create form straight from the schema const [form, setForm] = useState({ ...ArticleDefaults }) await strapi.articles.create({ ...ArticleDefaults, title: 'New Article' }) ``` Entities without any schema defaults get no constant — there is nothing to default. For components used in a **dynamic zone**, an additional `*DzDefaults` constant carries the `__component` discriminator, ready to push as a new block: ```ts import { HeroSectionDzDefaults } from './strapi/types' // HeroSectionDzDefaults === { __component: 'sections.hero', ...defaults } setBlocks(prev => [...prev, { ...HeroSectionDzDefaults }]) ``` --- --- url: /strapi-typed-client/guide/nextjs.md --- # Next.js Integration `strapi-typed-client` provides first-class Next.js support with automatic type generation during development, one-time generation during builds, and built-in cache options that map directly to Next.js fetch behavior. ## withStrapiTypes Wrapper Wrap your Next.js config with `withStrapiTypes` to enable automatic schema watching and type generation: ```ts // next.config.ts import { withStrapiTypes } from 'strapi-typed-client/next' const nextConfig = { // your existing Next.js config } export default withStrapiTypes({ strapiUrl: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337', token: process.env.STRAPI_TOKEN, output: './src/strapi', })(nextConfig) ``` ### Behavior | Mode | What Happens | | ------------ | ------------------------------------------------------------------------------ | | `next dev` | Connects to Strapi via SSE and regenerates types instantly when schema changes | | `next build` | Runs a one-time generation before the build starts | ### Options | Option | Description | Default | | ----------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | `strapiUrl` | Strapi server URL | `STRAPI_URL` env or `http://localhost:1337` | | `token` | API token | `STRAPI_TOKEN` env | | `silent` | Suppress generation logs | `false` | | `format` | `js` (compiled `.js` + `.d.ts`, default) or `ts` (raw `.ts` for bundlers/monorepos) | `js` | | `output` | Output directory — must point at your source tree (e.g. `./src/strapi`). The plugin throws a clear error if it is missing | required | ### Raw `.ts` output for bundlers and monorepos The generated client is always written into your source tree and committed. With `format: 'ts'` the plugin emits raw `.ts` instead of compiled `.js` + `.d.ts` — useful for monorepos where the shared types package compiles its own sources, or bundlers that prefer source: ```ts export default withStrapiTypes({ strapiUrl: process.env.STRAPI_URL ?? 'http://localhost:1337', token: process.env.STRAPI_TOKEN, format: 'ts', output: './src/strapi', })(nextConfig) ``` Then import from `@/strapi` (the `create-next-app` default `@/*` → `./src/*` path alias maps it to your output dir): ```ts import { StrapiClient } from '@/strapi' const strapi = new StrapiClient({ baseURL: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337', token: process.env.STRAPI_TOKEN, }) ``` Your `tsconfig.json` needs `moduleResolution: "bundler"` (the Next.js default) so the extensionless local imports inside the generated client resolve. ::: tip This replaces the need to run `strapi-types watch` or `strapi-types generate` manually. The wrapper handles everything. ::: ## Cache Options The `StrapiClient` methods accept an optional second parameter for Next.js cache configuration: ```ts interface NextOptions { revalidate?: number | false tags?: string[] cache?: RequestCache headers?: Record } ``` These options are passed directly to the `fetch` call through the `next` option that Next.js recognizes. ### ISR with Revalidate Use `revalidate` for Incremental Static Regeneration. The data is cached and revalidated at the specified interval (in seconds): ```ts const articles = await strapi.articles.find( { filters: { status: { $eq: 'published' } }, sort: ['publishedAt:desc'], }, { revalidate: 3600, // revalidate every hour }, ) ``` ### Cache Tags Tag your requests so you can revalidate them on demand using Next.js `revalidateTag()`: ```ts const articles = await strapi.articles.find( { populate: { category: true } }, { tags: ['articles'], revalidate: 3600, }, ) ``` Then in a Server Action or Route Handler: ```ts import { revalidateTag } from 'next/cache' export async function refreshArticles() { 'use server' revalidateTag('articles') } ``` ### No Cache For data that must always be fresh, disable caching entirely: ```ts const liveData = await strapi.articles.find({}, { cache: 'no-store' }) ``` ### Static Cache (Default) By default, Next.js caches fetch requests in the App Router. If you do not pass any cache options, the default Next.js caching behavior applies: ```ts // Cached by default in Next.js App Router const articles = await strapi.articles.find() ``` ## How It Works Next.js patches the global `fetch` function at runtime to add caching, deduplication, and revalidation. Since `StrapiClient` uses the global `fetch` by default, it automatically benefits from all Next.js optimizations: 1. **Request deduplication** — identical fetch calls in the same render pass are deduplicated 2. **Static caching** — responses are cached at build time for static pages 3. **ISR** — cached data is revalidated in the background at the specified interval 4. **Tag-based revalidation** — invalidate specific cached responses on demand The cache options you pass as the second parameter are forwarded to fetch like this: ```ts fetch(url, { next: { revalidate: options.revalidate, tags: options.tags, }, cache: options.cache, }) ``` ::: info This integration is completely optional. If you are not using Next.js, the second parameter is simply ignored and the client works with any standard `fetch` implementation. ::: ## Server Components Example In a Next.js App Router Server Component: ```tsx // app/articles/page.tsx import { StrapiClient } from '@/strapi' const strapi = new StrapiClient({ baseURL: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337', token: process.env.STRAPI_TOKEN, }) export default async function ArticlesPage() { const articles = await strapi.articles.find( { filters: { status: { $eq: 'published' } }, sort: ['publishedAt:desc'], populate: { category: true, coverImage: true }, }, { revalidate: 60, tags: ['articles'] }, ) return (
    {articles.map(article => (
  • {article.title}

    {article.category.name}
  • ))}
) } ``` ## Server Actions Example Use the client in Server Actions for mutations: ```tsx // app/articles/actions.ts 'use server' import { StrapiClient } from '@/strapi' import { revalidateTag } from 'next/cache' const strapi = new StrapiClient({ baseURL: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337', token: process.env.STRAPI_TOKEN, }) export async function createArticle(formData: FormData) { const result = await strapi.articles.create({ title: formData.get('title') as string, content: formData.get('content') as string, category: Number(formData.get('categoryId')), }) revalidateTag('articles') return result } export async function deleteArticle(documentId: string) { await strapi.articles.delete(documentId) revalidateTag('articles') } ``` ::: warning When using `StrapiClient` in Server Actions, make sure to only instantiate it on the server side. Do not expose your API token to the client bundle. ::: --- --- url: /strapi-typed-client/advanced/dynamic-zones.md --- # Dynamic Zones Dynamic Zones are one of Strapi's most powerful features, allowing content editors to compose pages from a set of predefined components. `strapi-typed-client` fully supports Dynamic Zones with generated union types, enabling type-safe access to heterogeneous content blocks. ## Generated Types Component interfaces describe the component shape itself — they intentionally **do not** include `__component`. The discriminator belongs to the place where the component is used, not the component definition. For every component that appears in at least one Dynamic Zone, the generator emits an extra `*Dz` alias that adds the `__component` literal. Dynamic Zone fields are typed as a union of those `*Dz` aliases. For example, given a `Landing` content type with a `content` Dynamic Zone that allows `HeroBlock`, `TextBlock`, and `GalleryBlock` components: ```typescript // Generated output in types.ts export interface HeroBlock { id: number heading: string subheading: string | null } export interface TextBlock { id: number body: string alignment: 'left' | 'center' | 'right' } export interface GalleryBlock { id: number title: string | null } export type HeroBlockDz = HeroBlock & { __component: 'landing.hero-block' } export type TextBlockDz = TextBlock & { __component: 'landing.text-block' } export type GalleryBlockDz = GalleryBlock & { __component: 'landing.gallery-block' } export interface Landing { id: number documentId: string title: string createdAt: string updatedAt: string } ``` Populatable fields — media, relations, nested components — live in `LandingGetPayload` rather than the base `Landing`, so the `content` field appears on populated payloads: ```typescript type LandingWithContent = LandingGetPayload<{ populate: { content: true } }> // LandingWithContent['content'] is (HeroBlockDz | TextBlockDz | GalleryBlockDz)[] ``` ::: tip Why two interfaces per component? The same component can be used both inside a Dynamic Zone and as a regular `type: "component"` attribute. Strapi only accepts `__component` in Dynamic Zones — for regular component attributes the discriminator is invalid and triggers a 400 on write. Keeping it on a separate `*Dz` alias lets the generator give both contexts the right input/output shapes. ::: ## Loading Dynamic Zone Content Dynamic Zone fields are **not populated by default** in Strapi responses. You must explicitly populate them to receive the component data: ```typescript const landing = await strapi.landing.find({ populate: { content: true, }, }) ``` For nested relations within Dynamic Zone components, use the `on` discriminator to specify per-component populate options: ```typescript const landing = await strapi.landing.find({ populate: { content: { on: { 'landing.hero-block': { populate: { backgroundImage: true }, }, 'landing.gallery-block': { populate: { images: true }, }, }, }, }, }) ``` The return type is fully inferred — populated relations within each component will include the full type instead of just `{ id, documentId }`. You can also combine `on` with `fields` to select specific fields per component: ```typescript const landing = await strapi.landing.find({ populate: { content: { on: { 'landing.hero-block': { fields: ['heading', 'subheading'], populate: { backgroundImage: true }, }, 'landing.text-block': { fields: ['body'], }, }, }, }, }) ``` ::: warning Without populating the Dynamic Zone field, it will be `null` in the response even if the entry has blocks configured in Strapi. ::: ## Type Narrowing Since each component carries a `__component` string literal, you can use it as a discriminant to narrow the union type. ### Using the `__component` field The most reliable approach is to check the `__component` field directly: ```typescript const landing = await strapi.landing.find({ populate: { content: true }, }) if (landing.content) { for (const block of landing.content) { switch (block.__component) { case 'landing.hero-block': // TypeScript knows `block` is HeroBlock here console.log(block.heading, block.subheading) break case 'landing.text-block': // TypeScript knows `block` is TextBlock here console.log(block.body, block.alignment) break case 'landing.gallery-block': // TypeScript knows `block` is GalleryBlock here console.log(block.title, block.images.length) break } } } ``` ### Using property checks You can also narrow using `in` checks on unique properties, though this is less precise: ```typescript if ('heading' in block) { // block is narrowed to HeroBlock (if `heading` is unique to it) console.log(block.heading) } ``` ::: tip Prefer `__component` checks over property checks. The `__component` field is always present and guaranteed to be unique, whereas property names may overlap across components. ::: ### Type guard helpers For reusable narrowing logic, create type guard functions. Use the `*Dz` alias as the predicate target so the discriminator is preserved through filtering: ```typescript type LandingBlock = LandingGetPayload<{ populate: { content: true } }>['content'][number] function isHeroBlock(block: LandingBlock): block is HeroBlockDz { return block.__component === 'landing.hero-block' } function isTextBlock(block: LandingBlock): block is TextBlockDz { return block.__component === 'landing.text-block' } // Usage const heroes = landing.content?.filter(isHeroBlock) ?? [] // heroes is typed as HeroBlockDz[] ``` ## Rendering Dynamic Zones in React A common pattern for rendering Dynamic Zones in a frontend framework: ```tsx import type { LandingGetPayload } from '@/strapi' type LandingBlocks = LandingGetPayload<{ populate: { content: true } }>['content'] const componentMap: Record> = { 'landing.hero-block': HeroSection, 'landing.text-block': TextSection, 'landing.gallery-block': GallerySection, } function DynamicZone({ blocks }: { blocks: LandingBlocks }) { if (!blocks) return null return ( <> {blocks.map((block, index) => { const Component = componentMap[block.__component] if (!Component) return null return })} ) } ``` ## Input Types for Dynamic Zones The generator emits `*DzInput` aliases for every component used in a Dynamic Zone. A Dynamic Zone input field is typed as `(BlockADzInput | BlockBDzInput | ...)[]` — each element requires `__component` so Strapi can route the block to the right component type: ```typescript await strapi.landings.create({ title: 'New Landing Page', content: [ { __component: 'landing.hero-block', heading: 'Welcome', subheading: 'Get started today', }, { __component: 'landing.text-block', body: 'This is the introduction paragraph.', alignment: 'center', }, ], }) ``` ::: warning Dynamic Zone vs regular component attributes `__component` is required **only** for Dynamic Zone payloads. For a regular `type: "component"` attribute (single or repeatable), Strapi already knows the component type from the schema and will reject `__component` in the payload with a 400. The generator reflects this: `*Input` for a regular component field does not include `__component`, while `*DzInput` for a Dynamic Zone block does. ::: When updating, you replace the entire Dynamic Zone array. Strapi does not support partial updates to individual blocks within a Dynamic Zone: ```typescript await strapi.landings.update(documentId, { content: [ { __component: 'landing.hero-block', heading: 'Updated Heading', subheading: null, }, // Only these blocks will exist after the update ], }) ``` ## Nullable Behavior * A Dynamic Zone field is `null` when it has no blocks or has not been populated. * An empty Dynamic Zone (all blocks removed) returns as an empty array `[]`. * Individual component fields within the zone follow their own nullability rules based on the Strapi schema. --- --- url: /strapi-typed-client/advanced/single-types.md --- # Single Types Single Types in Strapi represent unique, one-off entries rather than collections. Common examples include a homepage, site-wide settings, a navigation structure, or a footer. `strapi-typed-client` generates a simplified API surface for Single Types that reflects their singular nature. ## How Single Types Differ from Collection Types In Strapi, a Collection Type has many entries (like blog posts or products), while a Single Type has exactly one entry (like a homepage or global settings). The generated client reflects this distinction: | Method | Collection Type | Single Type | | ----------- | :-------------: | :------------------------: | | `find()` | Returns a list | Returns the single entry | | `findOne()` | Yes | No | | `create()` | Yes | No | | `update()` | Yes | Yes (no documentId needed) | | `delete()` | Yes | Yes (no documentId needed) | A Single Type API only exposes `find()`, `update()`, and `delete()` — there is no `findOne()` (since there is only one entry) and no `create()` (the entry is created automatically by Strapi). ## Generated Client API For a Single Type called `Landing`, the generated client provides: ```typescript // The client exposes a simplified API for single types const strapi = new StrapiClient({ baseURL: 'http://localhost:1337' }) // Fetch the single entry const landing = await strapi.landing.find() // Fetch with populate const landing = await strapi.landing.find({ populate: { hero: true, seo: true, }, }) // Update the single entry (no documentId required) await strapi.landing.update({ title: 'Updated Landing Title', }) // Delete the single entry await strapi.landing.delete() ``` ::: info The `find()` method on a Single Type resolves to a single object, not an array. Strapi's `GET /api/landing` returns `{ data: { ... } }` over the wire; the client unwraps the envelope, so `find()` gives you the object directly. ::: ## Response Shape A Single Type `find()` resolves to the entry object directly — the client unwraps Strapi's `{ data }` envelope: ```typescript const result = await strapi.landing.find() // result is the entry object, not an array console.log(result.title) console.log(result.documentId) ``` Compare this with a Collection Type: ```typescript // Collection type — find() returns an array const posts = await strapi.posts.find() console.log(posts[0].title) // Single type — find() returns a single object const landing = await strapi.landing.find() console.log(landing.title) ``` ## Populate Populate works the same way as with Collection Types. You can populate relations, components, media fields, and dynamic zones: ```typescript // Boolean populate for a specific field const landing = await strapi.landing.find({ populate: { hero: true, }, }) // Deep populate for nested relations const landing = await strapi.landing.find({ populate: { hero: { populate: { backgroundImage: true, }, }, features: { populate: { icon: true, }, }, seo: true, }, }) ``` ## Field Selection You can select specific fields to reduce the response payload: ```typescript const landing = await strapi.landing.find({ fields: ['title', 'description'], }) ``` ## Updating a Single Type Since there is only one entry, you do not need to provide a `documentId`: ```typescript await strapi.landing.update({ title: 'New Title', description: 'Updated description for the landing page.', }) ``` ::: tip You only need to include the fields you want to change. Strapi performs a partial update, leaving other fields unchanged. ::: ## Next.js Integration Single Types work well with Next.js caching, especially for pages that change infrequently: ```typescript // Cache the landing page for 1 hour with ISR const landing = await strapi.landing.find( { populate: { hero: true, features: true } }, { revalidate: 3600, tags: ['landing'] }, ) // Force fresh data (no cache) const landing = await strapi.landing.find( { populate: { hero: true } }, { cache: 'no-store' }, ) ``` ## Full Example A complete example fetching and rendering a landing page in Next.js: ```typescript import { StrapiClient } from '@/strapi' const strapi = new StrapiClient({ baseURL: process.env.NEXT_PUBLIC_STRAPI_URL ?? 'http://localhost:1337', token: process.env.STRAPI_TOKEN, }) export default async function LandingPage() { const landing = await strapi.landing.find({ populate: { hero: { populate: { backgroundImage: true, }, }, features: { populate: { icon: true, }, }, seo: true, }, }); return (

{landing.title}

{landing.hero && (

{landing.hero.heading}

{landing.hero.subheading}

)}
{landing.features?.map((feature, i) => (

{feature.title}

{feature.description}

))}
); } ``` --- --- url: /strapi-typed-client/advanced/authentication.md --- # Authentication `strapi-typed-client` supports Bearer token authentication for both the runtime client and the CLI code generator. This page covers all the ways to configure authentication. ## Client Authentication ### Token via Constructor The most common approach is to pass the API token when creating the client: ```typescript import { StrapiClient } from '@/strapi' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', token: 'your-strapi-api-token', }) ``` The token is sent as a `Bearer` token in the `Authorization` header with every request. ### Setting Token Dynamically You can set or update the token after the client has been created using the `setToken` method: ```typescript const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', }) // Set token later (e.g., after user login) strapi.setToken('your-strapi-api-token') // Now all subsequent requests include the token const data = await strapi.posts.find() ``` This is useful in scenarios where the token is not available at initialization time, such as after a user authenticates. ### Using Environment Variables A recommended pattern is to read the token from an environment variable: ```typescript const strapi = new StrapiClient({ baseURL: process.env.NEXT_PUBLIC_STRAPI_URL!, token: process.env.STRAPI_TOKEN, }) ``` ::: tip In Next.js, environment variables without the `NEXT_PUBLIC_` prefix are only available on the server side. Since API tokens should remain secret, use `STRAPI_TOKEN` (without the prefix) and only create the client in server components or API routes. ::: Example `.env.local` file: ```bash NEXT_PUBLIC_STRAPI_URL=http://localhost:1337 STRAPI_TOKEN=your-strapi-api-token-here ``` ## Creating API Tokens in Strapi To generate an API token in the Strapi admin panel: 1. Navigate to **Settings** in the left sidebar. 2. Under **Global Settings**, click **API Tokens**. 3. Click **Create new API Token**. 4. Configure the token: * **Name**: A descriptive name (e.g., "Frontend Read-Only"). * **Token type**: Choose `Read-only`, `Full access`, or `Custom`. * **Token duration**: Set an expiration or choose unlimited. 5. Click **Save** and copy the generated token. ::: warning The token is only displayed once after creation. Store it securely in your environment variables or a secrets manager. If you lose it, you will need to regenerate a new token. ::: ### Token Types | Type | Permissions | Use Case | | ----------- | ----------------------------------- | ----------------------------- | | Read-only | `find` and `findOne` on all content | Public frontend, SSG/ISR | | Full access | All CRUD operations on all content | Admin dashboards, server-side | | Custom | Fine-grained per-content-type | Specific use cases | ## CLI Authentication ### Plugin `requireAuth` Option The `strapi-typed-client` plugin can be configured to require authentication for its schema endpoint. When enabled, the CLI must provide a valid token to fetch the schema. In your Strapi project's `config/plugins.ts`: ```typescript // config/plugins.ts export default { 'strapi-typed-client': { enabled: true, config: { requireAuth: true, // Require Bearer token for schema endpoint }, }, } ``` By default, `requireAuth` is `false` in development and `true` in production (`NODE_ENV === 'production'`). In development, the schema endpoint is publicly accessible for convenience. ### Passing a Token to the CLI When the plugin has `requireAuth: true`, pass the token using the `--token` flag: ```bash npx strapi-types generate --token YOUR_API_TOKEN ``` Or using an environment variable: ```bash STRAPI_TOKEN=your-token npx strapi-types generate ``` ::: info The CLI uses the same Bearer token mechanism as the runtime client. Any API token with at least read access will work for schema fetching. ::: ### Full CLI Example ```bash # Without auth (requireAuth: false, the default) npx strapi-types generate --url http://localhost:1337 # With auth (requireAuth: true) npx strapi-types generate --url http://localhost:1337 --token your-api-token # Using environment variable STRAPI_TOKEN=your-api-token npx strapi-types generate --url http://localhost:1337 ``` ## Security Recommendations ### Development During local development, `requireAuth` defaults to `false` so you can regenerate types without managing tokens: ```typescript // config/plugins.ts export default { 'strapi-typed-client': { enabled: true, config: { requireAuth: false, }, }, } ``` ### Production In production, `requireAuth` defaults to `true` automatically. You can also set it explicitly: ```typescript // config/plugins.ts export default { 'strapi-typed-client': { enabled: true, config: { requireAuth: true, }, }, } ``` ::: warning The schema endpoint exposes the complete structure of your content types, including field names, relation targets, and component structures. In production environments, enable `requireAuth` to prevent this information from being publicly accessible. ::: ### CI/CD Pipelines When generating types in a CI/CD pipeline, store the token as a secret and pass it to the CLI: ```yaml # GitHub Actions example - name: Generate Strapi types run: npx strapi-types generate --url ${{ secrets.STRAPI_URL }} --token ${{ secrets.STRAPI_TOKEN }} ``` ## Authentication Flow Summary ``` ┌─────────────────────────────────────────────────┐ │ Strapi Server │ │ │ │ config/plugins.ts │ │ ┌─────────────────────────────────────────┐ │ │ │ requireAuth: true/false │ │ │ └─────────────────────────────────────────┘ │ │ │ │ GET /api/strapi-typed-client/schema │ │ ← Authorization: Bearer │ │ │ │ GET /api/strapi-typed-client/schema-hash │ │ ← Authorization: Bearer │ │ │ └────────────────────┬────────────────────────────┘ │ ┌────────────┼────────────────┐ │ │ │ CLI Generate StrapiClient CI/CD Pipeline --token flag { token: '...' } env secret ``` --- --- url: /strapi-typed-client/advanced/custom-endpoints.md --- # Custom Endpoints `strapi-typed-client` generates typed methods for your custom Strapi controllers and routes — not just standard CRUD. You describe each endpoint's request and response once, in the controller, and the generated client exposes a fully typed method for it. ## Mental model There are three pieces, and it helps to keep them straight: 1. **You write** an `export interface Endpoints` in your controller, declaring the `body` and `response` of each custom action. 2. **The plugin scrapes** that interface (and your route files) at generation time and emits, per controller, an `export namespace API { ... }` block of `*Request` / `*Response` types plus a typed method on the client. 3. **You call** the generated method — `strapi.articles.publish(...)` — and get autocomplete and a typed return. ::: warning `Endpoints` is yours, not the generator's Nothing named `Endpoints` appears in the generated output. It's a declaration the plugin reads from your source; the output is the `API` namespace and the client method. Renaming or deleting `Endpoints` only changes what the generator can see. ::: The plugin reads three things from your project: * **Routes** — `src/api/*/routes/*.ts` for each endpoint's method, path, and handler. * **The `Endpoints` interface** — request/response types per action. * **Extra exported types** — any other `export type` / `export interface` in the controller, hoisted into the namespace. ## Defining a typed endpoint ### 1. Declare the route ```typescript // src/api/article/routes/custom-routes.ts export default { routes: [ { method: 'POST', path: '/articles/:id/publish', handler: 'article.publish', }, { method: 'GET', path: '/articles/trending', handler: 'article.trending', }, ], } ``` The handler's action segment (`publish`, `trending`) is the key you'll use in `Endpoints`. Short, `api::`, and `plugin::` handler forms are all supported: ```typescript handler: 'article.publish' // short handler: 'api::article.article.publish' // full UID handler: 'plugin::my-plugin.controller.action' // plugin ``` ### 2. Declare types via the `Endpoints` interface Each key must match a handler action. Only `body` and `response` are read (see [Field reference](#field-reference)): ```typescript // src/api/article/controllers/article.ts import { factories } from '@strapi/strapi' export interface Endpoints { publish: { body: { scheduledAt?: string; notifySubscribers: boolean } response: { data: { publishedAt: string; url: string } } } trending: { response: { data: { id: number; title: string; views: number }[] } } } export default factories.createCoreController( 'api::article.article', ({ strapi }) => ({ async publish(ctx) { // ...business logic ctx.body = { data: { publishedAt: '...', url: '...' } } }, async trending(ctx) { ctx.body = { data: [{ id: 1, title: '...', views: 1500 }] } }, }), ) ``` ### 3. What gets generated For a `checkout` controller with a `buyPlan` action, the generator emits a namespace and a typed method: ```typescript export namespace CheckoutAPI { export type BuyPlanRequest = { planId: string; coupon?: string } export type BuyPlanResponse = { url: string } } // on the client: async buyPlan(data?: CheckoutAPI.BuyPlanRequest | FormData): Promise ``` ## Consuming from the client Call the method off the matching collection (or standalone) API. Path params come first, the body last; the return is the **unwrapped** response: ```typescript const result = await strapi.articles.publish('article-id', { notifySubscribers: true, }) result.publishedAt // string result.url // string const trending = await strapi.articles.trending() trending[0].title // string ``` The namespace types are exported too, so you can reference them directly: ```typescript import type { CheckoutAPI } from '@/strapi' function buildCheckout(): CheckoutAPI.BuyPlanRequest { return { planId: 'pro' } } ``` ## Field reference Each action in `Endpoints` is read for exactly two fields: | Field | Effect | | ---------- | ----------------------------------------------------------------------------------- | | `body` | Request body type. Becomes the `data?` argument on POST/PUT/PATCH methods. | | `response` | Response type. A single top-level `{ data: ... }` wrapper is unwrapped (see below). | ::: warning `params` and `query` are not wired up You may see `params` / `query` in older examples, but the generator **parses and then discards them** — they have no effect on the generated method today. Path parameters come from the route (`:id`), and query strings are not typed. Don't rely on declaring them. ::: ## Response unwrapping Strapi controllers usually return `{ data: ... }`. The generator strips a **single top-level `data` wrapper** so the method returns the inner type directly: ```typescript response: { data: { url: string } } // method returns { url: string } response: { url: string } // returned as-is (no wrapper to strip) response: void // method returns void ``` Only one top-level `{ data: ... }` is unwrapped; nested `data` keys and non-`data` shapes pass through unchanged. ## Method signatures * **Path parameters** become positional `string` arguments, in path order. `/teams/:teamId/members/:memberId` → `getMember(teamId: string, memberId: string)`. * **POST / PUT / PATCH** get a trailing `data?: | FormData` argument. * **GET / DELETE** take no body argument. * The return is always `Promise<>`, unwrapped. * Without an `Endpoints` entry the method is still generated, but input and output fall back to `any`. ## Reserved CRUD action names If a content-type controller action is named `find`, `findOne`, `findWithMeta`, `create`, `update`, or `delete`, generating a method with that exact name would clash with the typed base CRUD method. The generator **mangles** it to ``: ```typescript // a `create` action on the article controller → await strapi.articles.createArticle(data) // the typed base method stays available: await strapi.articles.create({ title: 'x' }) ``` Standalone APIs (next section) extend a base with no CRUD surface, so their methods are never mangled. ## Referencing named types & the `unknown` fallback Inside `body` / `response` you can reference: generated content-type and component names, `MediaFile`, the sibling `*Request` / `*Response` types, TypeScript built-ins, and any `export type` / `export interface` declared in the **same** controller (see [Extra exported types](#extra-exported-types)). Anything else — most commonly a type `import`ed from another file — can't be resolved from the schema, so it **degrades to `unknown`** and the generator leaves a discoverable note in the committed output: ```typescript // NOTE: GenerationView could not be resolved from the schema and fall back to `unknown` (custom-endpoint types referencing controller-local types). export type GetXResponse = { item: Item; widget: unknown } ``` To fix it, inline the shape in the `Endpoints` declaration or re-declare it as an `export`ed type in the same controller. ## Extra exported types Any standalone `export type` / `export interface` in a controller (other than `Endpoints`) is hoisted into the namespace, so your endpoint types can reference it: ```typescript // src/api/search/controllers/search.ts export type SearchResultType = 'article' | 'page' | 'product' export interface SearchHit { id: number title: string type: SearchResultType score: number } export interface Endpoints { query: { body: { term: string; filters?: { type?: SearchResultType } } response: { data: SearchHit[] } } } ``` generates: ```typescript export namespace SearchAPI { export type SearchResultType = 'article' | 'page' | 'product' export interface SearchHit { id: number title: string type: SearchResultType score: number } export type QueryRequest = { term: string filters?: { type?: SearchResultType } } export type QueryResponse = SearchHit[] } ``` ## Standalone APIs (no content type) A controller whose name matches a content type's `singularName` attaches its methods to that collection. Otherwise it becomes a standalone `strapi.` class: ```typescript // src/api/contact/routes/custom-routes.ts → strapi.contact.submit(...) await strapi.contact.submit({ name: 'Jane', email: 'jane@example.com', message: 'Hi', }) ``` ## FormData & file uploads Custom methods accept either JSON or `FormData`, which is what you want for uploads: ```typescript const formData = new FormData() formData.append('file', csvFile) await strapi.imports.upload(formData) ``` ## Authoring constraints The plugin scrapes `body` / `response` with a regex + balanced-brace scanner — **not** the TypeScript compiler. Keep declarations in a shape it can read: * Use an **inline object literal** (`{ ... }`) or a **single simple token** (`void`, a type name) for `body` / `response`. * Each `Endpoints` key must **equal the handler's action name**. * Avoid splitting a type across constructs the scanner can truncate (e.g. a `body` assembled from external generics). ::: tip This describes the current regex-based parser. A future release moves to a TypeScript-AST parser, which will widen what resolves and shrink the `unknown` fallback — your `Endpoints` declarations won't need to change. ::: For the generate / watch / commit workflow, see the [CLI reference](/guide/cli). --- --- url: /strapi-typed-client/advanced/extending-payloads.md --- # Extending Payload Types `*GetPayload` mirrors your Strapi schema exactly. When a controller returns more than the schema describes — computed fields, narrower variants of an existing field — a direct `as Domain` cast won't compile: ```typescript type Share = Omit< ProjectGetPayload<{ populate: { config: true } }>, 'config' > & { config: ProjectShareConfig[] watermark: boolean authorUsername: string } const data = (await strapi.projects.findOne(documentId, { populate: { config: true }, })) as Share // TS2352: neither type sufficiently overlaps with the other. ``` `Omit` removes `config`, the intersection adds a different `config`, and the resulting `Share` is no longer a subtype of `ProjectGetPayload`. TypeScript correctly refuses the cross-cast. The recipes below cover the practical ways to bridge the gap. ## Spread With Per-Field Cast For most cases this is the shortest path. Spread the payload, declare the additions and narrowings explicitly: ```typescript const data = await strapi.projects.findOne(documentId, { populate: { item: { populate: ['category'] }, config: true }, }) if (!data) return null const share: Share = { ...data, watermark: computeWatermark(data), authorUsername: getAuthorUsername(data), refCode: getRefCode(data), config: data.config as ProjectShareConfig[], } ``` The cast is narrow and local to the one field that genuinely diverges from the schema. TypeScript still checks every other field against `Share`. ## Intersection When You Only Add Fields If the domain type purely **adds** fields — no narrowing of existing ones — a plain intersection compiles without any cast: ```typescript type ShareLite = ProjectPayload & { watermark: boolean authorUsername: string } const share = (await strapi.projects.findOne(...)) as ShareLite ``` `ShareLite` is a subtype of `ProjectPayload`, so `as` is valid. ## `as unknown as Domain` at the Boundary When the gap is large enough that spread feels awkward, a double cast at the repository boundary is a fine pragmatic choice: ```typescript async function findShare(documentId: string): Promise { const data = await strapi.projects.findOne(documentId, { populate: { item: { populate: ['category'] }, config: true }, }) return data as unknown as Share } ``` Keep it at the boundary (one place per domain type, in the repository or data-access layer). The signature of `findShare` carries the `Share` contract from that point on, so the rest of the codebase stays cast-free. ## Mapper Function When You Want Explicitness If you'd rather see every domain field assigned by hand — useful when the mapping is non-trivial or when reviewers benefit from the cross-reference — write a mapper: ```typescript function toShare( payload: ProjectPayload, computed: { watermark: boolean; authorUsername: string; refCode?: string }, ): Share { return { documentId: payload.documentId, item: payload.item, config: payload.config as ProjectShareConfig[], ...computed, } } ``` This makes schema drift impossible to miss: rename a field in Strapi and the mapper fails to compile immediately. --- --- url: /strapi-typed-client/advanced/plugin-config.md --- # Plugin Configuration The `strapi-typed-client` package includes a Strapi plugin that exposes your content type schema via HTTP endpoints. The CLI generator uses these endpoints to fetch the schema and generate TypeScript types. This page covers the full plugin setup and configuration options. ## Installation The plugin is included in the `strapi-typed-client` package. No separate installation is needed — just add `strapi-typed-client` to your Strapi project's dependencies. ## Registering the Plugin Strapi auto-discovers the plugin thanks to the `strapi` field in `package.json`. You only need to enable it in `config/plugins.ts`: ```typescript // config/plugins.ts export default { 'strapi-typed-client': { enabled: true, }, } ``` With configuration options: ```typescript // config/plugins.ts export default { 'strapi-typed-client': { enabled: true, config: { requireAuth: true, }, }, } ``` ::: info No `import` or `resolve` is needed. Strapi detects the plugin automatically when the package is installed and the `strapi.kind === 'plugin'` field is present in its `package.json`. ::: ## Configuration Options ### `requireAuth` * **Type:** `boolean` * **Default:** `false` in development, `true` in production (`NODE_ENV === 'production'`) Controls whether the schema endpoints require a valid Bearer token in the `Authorization` header. ```typescript // Explicit public access 'strapi-typed-client': { enabled: true, config: { requireAuth: false, }, } // Explicit protected access 'strapi-typed-client': { enabled: true, config: { requireAuth: true, }, } ``` When `requireAuth` is `true`: * All schema endpoint requests must include a valid `Authorization: Bearer ` header. * Unauthenticated requests receive a `401 Unauthorized` response. * The CLI must be invoked with the `--token` flag or have the `STRAPI_TOKEN` environment variable set. ::: warning If you do not set `requireAuth` explicitly, it defaults based on `NODE_ENV`. In production your schema endpoint is protected automatically. Set `requireAuth: false` explicitly if you want to keep it public in production (not recommended). ::: ## Plugin Endpoints The plugin registers two endpoints under the `/api/strapi-typed-client` namespace. ### `GET /api/strapi-typed-client/schema` Returns the complete content type schema as JSON, along with a hash of the schema. **Response format:** ```json { "hash": "a1b2c3d4e5f6...", "schema": { "contentTypes": { "api::post.post": { "kind": "collectionType", "singularName": "post", "pluralName": "posts", "attributes": { "title": { "type": "string", "required": true }, "content": { "type": "richtext" }, "author": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.user" } } } }, "components": { "landing.hero-block": { "attributes": { "heading": { "type": "string" }, "backgroundImage": { "type": "media" } } } } } } ``` **Example request:** ```bash # Without auth curl http://localhost:1337/api/strapi-typed-client/schema # With auth curl -H "Authorization: Bearer YOUR_TOKEN" \ http://localhost:1337/api/strapi-typed-client/schema ``` ### `GET /api/strapi-typed-client/schema-hash` Returns only the schema hash. Useful for one-off checks or CI pipelines. **Response format:** ```json { "hash": "a1b2c3d4e5f6..." } ``` **Example request:** ```bash curl http://localhost:1337/api/strapi-typed-client/schema-hash ``` ### `GET /api/strapi-typed-client/schema-watch` SSE (Server-Sent Events) stream for watch mode. On connect, immediately sends the current schema hash. When the Strapi server restarts, the connection drops and the client reconnects automatically. **Event format:** ``` event: connected data: {"hash":"a1b2c3d4e5f6..."} ``` The CLI and `withStrapiTypes` wrapper use this endpoint in watch mode instead of polling. ## Creating an API Token When `requireAuth` is enabled, the CLI and any direct endpoint calls need a Bearer token. Here's how to create one: 1. Open the **Strapi admin panel** (usually `http://localhost:1337/admin`). 2. Go to **Settings** in the left sidebar. 3. Under **Global Settings**, click **API Tokens**. 4. Click **Create new API Token**. 5. Fill in the details: * **Name** — descriptive name (e.g., "Types Generator", "CI/CD Schema") * **Token type** — `Read-only` is sufficient for schema fetching * **Token duration** — choose `Unlimited` for development, set an expiration for production 6. Click **Save**. 7. **Copy the token immediately** — it is only shown once. ::: warning Store the token securely. If you lose it, you will need to regenerate a new one. Never commit tokens to version control — use environment variables or a secrets manager. ::: ### Token Types | Type | Permissions | Best For | | ----------- | ----------------------------------------- | -------------------------------------- | | Read-only | `find` and `findOne` on all content types | Schema fetching (CLI), public frontend | | Full access | All CRUD operations on all content types | Admin dashboards, server-side apps | | Custom | Fine-grained per-content-type | Specific use cases | For the CLI type generation, **Read-only** is sufficient since it only reads the schema. ## Schema Hashing The plugin computes a deterministic hash of the entire schema (content types + components). This hash is used by the CLI to avoid unnecessary regeneration. ### How It Works 1. The plugin serializes the full schema (content types and components) into a stable JSON string. 2. It computes a hash (SHA-256) of that string. 3. The hash is included in both the `/schema` and `/schema-hash` responses. ### CLI Usage The CLI stores the hash of the last generated schema. On subsequent runs, it first calls `/schema-hash` to check if the schema has changed: ``` CLI Strapi Plugin │ │ │ GET /schema-hash │ │────────────────────────────────────►│ │ │ │ { "hash": "abc123" } │ │◄────────────────────────────────────│ │ │ │ Compare with stored hash │ │ │ │ [If different] │ │ GET /schema │ │────────────────────────────────────►│ │ │ │ { "hash": "abc123", "schema": {…}} │ │◄────────────────────────────────────│ │ │ │ Generate types │ │ Store new hash │ ``` This means: * If the schema has not changed, the CLI skips regeneration entirely. * Only a tiny JSON payload is transferred on each check. * In watch mode, the CLI connects to the SSE endpoint `/schema-watch` and receives the hash on connect. When the Strapi server restarts, the connection drops and the client reconnects — receiving the (potentially new) hash automatically. ::: tip The SSE approach makes watch mode more efficient than polling — no repeated requests, instant detection of schema changes on server restart. ::: ## Security Considerations The schema endpoint exposes the complete structure of your Strapi content model, including: * All content type names and their fields * Field types and validation rules * Relation targets and cardinality * Component structures * Enumeration values ### When to Enable `requireAuth` | Environment | Recommendation | Reason | | ----------------- | -------------------- | -------------------------------------------- | | Local development | `requireAuth: false` | Convenience; no tokens to manage | | Staging | `requireAuth: true` | Prevent leaking schema to unauthorized users | | Production | `true` (default) | Schema structure should not be public | | CI/CD | `requireAuth: true` | Use secrets for token management | ::: warning With `requireAuth: false`, anyone with network access to your Strapi instance can view the full schema. In any environment accessible beyond your local machine, keep `requireAuth` enabled (the production default). ::: ### Network-Level Protection In addition to `requireAuth`, consider network-level protections: * Run Strapi behind a reverse proxy and restrict `/api/strapi-typed-client/*` routes to trusted IPs. * In CI/CD, use internal network addresses or VPN to access the Strapi instance. * Use short-lived API tokens where possible. ## Full Configuration Example A complete `config/plugins.ts` showing the plugin alongside other common Strapi plugins: ```typescript export default ({ env }) => ({ // strapi-typed-client plugin — auto-discovered, just enable + configure 'strapi-typed-client': { enabled: true, config: { requireAuth: env('NODE_ENV') === 'production', }, }, // Other plugins upload: { config: { provider: 'aws-s3', }, }, email: { config: { provider: 'sendgrid', }, }, }) ``` ::: tip Using `env('NODE_ENV') === 'production'` makes the behavior explicit in your config, matching the default behavior. This way it's clear to anyone reading the config what happens in each environment. ::: ## Troubleshooting ### 401 Unauthorized when generating types The plugin has `requireAuth: true` (or you're in production where it defaults to `true`) and you are not providing a token. Pass the token to the CLI: ```bash npx strapi-types generate --url http://localhost:1337 --token YOUR_TOKEN ``` ### 404 Not Found on schema endpoint The plugin is not registered or not enabled. Verify that: 1. `strapi-typed-client` is in your Strapi project's `dependencies` (not just `devDependencies`). 2. `config/plugins.ts` has `'strapi-typed-client': { enabled: true }`. 3. You have restarted Strapi after changing the config. ### Schema hash does not change after modifying a content type Restart your Strapi development server. The plugin reads the schema from Strapi's runtime registry, which updates on server start. --- --- url: /strapi-typed-client/reference/type-mapping.md --- # Type Mapping This page is a comprehensive reference for how Strapi schema types are converted to TypeScript types during code generation. ## Scalar Type Mapping | Strapi Type | TypeScript Type | Notes | | ------------ | --------------- | ------------------------------------ | | `string` | `string` | Short text field | | `text` | `string` | Long text field | | `richtext` | `string` | Markdown rich text (Strapi v4 style) | | `email` | `string` | Email field | | `uid` | `string` | Unique identifier field | | `integer` | `number` | | | `biginteger` | `number` | | | `float` | `number` | | | `decimal` | `number` | | | `boolean` | `boolean` | | | `date` | `string` | ISO date string (`YYYY-MM-DD`) | | `datetime` | `string` | ISO datetime string | | `time` | `string` | Time string (`HH:mm:ss`) | | `json` | `unknown` | Arbitrary JSON data | ## Complex Type Mapping | Strapi Type | TypeScript Type | Notes | | ------------------------------ | --------------------------- | ------------------------------------------------------------------- | | `enumeration<['a', 'b', 'c']>` | `'a' \| 'b' \| 'c'` | Union of literal string types | | `blocks` (Rich Text v2) | `BlocksContent` | Structured block array; see [Media & Blocks](/reference/media-file) | | `media` (single) | `MediaFile \| null` | See [MediaFile](/reference/media-file) | | `media` (multiple) | `MediaFile[]` | Array of media objects | | `component` (single) | `ComponentName` | Typed interface for the component | | `component` (repeatable) | `ComponentName[]` | Array of component objects | | `dynamiczone` | `(CompA \| CompB \| ...)[]` | Union type array of all allowed components | | `password` | excluded | Private fields are not generated | ## Relation Mapping Relations are mapped differently depending on whether they are populated or not. ### Base Types (without populate) Relations are **not included** in base types. When you query without `populate`, Strapi does not return relation data, so the generated base interface only contains scalar fields. ### Populated Types (with populate) When you use the `populate` parameter, the return type automatically includes the related entities: | Relation Type | Populated TypeScript Type | | ------------- | ------------------------- | | `oneToOne` | `RelatedType \| null` | | `manyToOne` | `RelatedType \| null` | | `oneToMany` | `RelatedType[]` | | `manyToMany` | `RelatedType[]` | ```ts // Without populate — only scalar fields const article = await strapi.articles.findOne('abc123') // article: Article (no relations) // With populate — relations are included in the type const article = await strapi.articles.findOne('abc123', { populate: { category: true, tags: true }, }) // article: Article & { category?: Category | null; tags?: Tag[] } ``` ### Input Types (create/update) In input types, every relation is typed as `RelationInput` (`StrapiID | StrapiID[] | RelationOperations | null`, where `StrapiID = string | number`). A plain id or array is shorthand for `set`; the explicit `{ connect | disconnect | set }` form is also accepted: | Relation Type | Input TypeScript Type | | ------------- | --------------------- | | `oneToOne` | `RelationInput` | | `manyToOne` | `RelationInput` | | `oneToMany` | `RelationInput` | | `manyToMany` | `RelationInput` | ## Base Fields The following fields are automatically added to every generated content type interface: | Field | Type | Description | | ------------ | -------- | ----------------------------- | | `id` | `number` | Auto-incremented database ID | | `documentId` | `string` | Strapi v5 document identifier | | `createdAt` | `string` | ISO datetime of creation | | `updatedAt` | `string` | ISO datetime of last update | Component types receive only `id: number` as a base field. ::: info A readonly `__typename` field is also added to content type interfaces for nominal typing. This ensures TypeScript treats structurally similar types as distinct. You do not need to use this field directly. ::: ## Excluded Fields The following fields from the Strapi schema are **not** included in generated types: | Field / Attribute | Reason | | ---------------------------- | --------------------------------------------- | | `createdBy` | Admin-only field | | `updatedBy` | Admin-only field | | `publishedAt` | Managed by Strapi internally | | `password` | Private attribute | | Admin relations (`admin::*`) | Admin panel internals | | Non-user plugin relations | Plugin internals (except `users-permissions`) | ::: info i18n Fields If your content type has the Strapi i18n plugin enabled, `locale` (string) and `localizations` (self-referencing relation) are **automatically included** in generated types. Content types without i18n are not affected. ::: ::: tip Any attribute marked as `private` in the Strapi schema is automatically excluded from generated types. The `password` type is the most common example. ::: ## Nullable and Optional Behavior Nullability depends on the `required` setting in your Strapi schema and the type category: ### Base Types (reading) * **Required fields** are generated as their plain type (e.g., `title: string`). * **Non-required fields** are generated with `| null` (e.g., `description: string | null`). * Relations already encode nullability in their type (`| null` for singular, `[]` for plural). ### Input Types (writing) * **All fields** are optional (`?:`) because input types are used for both create and partial update operations. * Scalar fields use `| null` to allow clearing a value (e.g., `title?: string | null`). * Relation fields are typed `RelationInput`: `category?: RelationInput` (accepts an id/documentId, an array, or `{ connect | disconnect | set }`). * Media fields are typed `MediaInput` (single) or `MultiMediaInput` (multiple): `avatar?: MediaInput`, `gallery?: MultiMediaInput`. * Component fields accept objects: `seo?: SeoComponentInput | null`. ```ts // All input fields are optional for partial updates interface ArticleInput { title?: string | null body?: string | null category?: RelationInput // relation (id, documentId, array, or operations) cover?: MediaInput // media by id seo?: SeoComponentInput | null // component as object tags?: RelationInput // relation (any cardinality) } ``` --- --- url: /strapi-typed-client/reference/generated-types.md --- # Generated Types This page explains the structure of the TypeScript files produced by the `strapi-types generate` command and the type categories they contain. ## Output Files By default, generated files are written to `./dist`. You can change this with the `--output` flag. | File | Description | | ------------- | ------------------------------------------------------------------------------------------------------- | | `types.d.ts` | All TypeScript interfaces: base types, input types, payload types, components, filters, populate params | | `client.js` | The `StrapiClient` class with typed methods for every content type (also exports `SCHEMA_HASH`) | | `client.d.ts` | Type declarations for the client | | `index.js` | Re-exports everything from `types` and `client` | | `index.d.ts` | Type declarations for the index | ``` dist/ types.d.ts # Type definitions client.js # Runtime client code + SCHEMA_HASH constant client.d.ts # Client type declarations index.js # Re-export barrel index.d.ts # Index type declarations ``` ## Type Categories The generator produces three categories of types for each content type, plus supporting utility types. ### 1. Base Types Base types represent the shape of data **returned by Strapi** for read operations (without populate). They contain only scalar fields and the standard base fields. ```ts export interface Article { readonly __typename?: 'Article' id: number documentId: string createdAt: string updatedAt: string title: string slug: string body: string | null publishDate: string | null views: number status: 'draft' | 'published' | 'archived' } ``` Key characteristics: * Includes `id`, `documentId`, `createdAt`, `updatedAt` on every type. * Scalar fields only -- relations, media, components, and dynamic zones are **not** present. * Non-required fields have `| null`. * Enumerations become union literal types. * A readonly `__typename` field provides nominal typing to distinguish structurally similar interfaces. ### 2. Input Types Input types are used for `create` and `update` operations. Every field is optional to support partial updates. ```ts export interface ArticleInput { title?: string | null slug?: string | null body?: string | null publishDate?: string | null views?: number | null status?: 'draft' | 'published' | 'archived' | null category?: number | null // relation -> ID cover?: number | null // media -> ID gallery?: number[] | null // multiple media -> ID array tags?: number[] | null // many relation -> ID array seo?: SeoComponentInput | null // component -> nested input object blocks?: (HeroInput | CtaInput)[] | null // dynamic zone -> union input array } ``` Key characteristics: * All fields are optional (`?:`). * Relations accept numeric IDs (not full objects). * Media fields accept numeric IDs. * Components use their corresponding `*Input` type. * Dynamic zones use a union of `*Input` types. ### 3. Payload Types (GetPayload) Payload types use TypeScript generics to resolve the return type based on your `populate` parameter. This is what enables type-safe populate inference. ```ts export type ArticleGetPayload

= Article & (P extends { populate: infer Pop } ? Pop extends '*' | true ? { category?: Category | null cover?: MediaFile tags?: Tag[] blocks?: (Hero | Cta)[] } : Pop extends readonly (infer _)[] ? { category?: 'category' extends Pop[number] ? Category | null : never cover?: 'cover' extends Pop[number] ? MediaFile : never // ... } : { category?: 'category' extends keyof Pop ? /* nested resolve */ Category | null : never // ... } : {}) ``` The payload type handles three populate styles: | Populate Style | Example | Behavior | | -------------- | ---------------------------------------------------------- | ------------------------------------- | | Wildcard | `populate: '*'` or `populate: true` | All populatable fields included | | Array | `populate: ['category', 'cover']` | Only named fields included | | Object | `populate: { category: true, cover: { fields: ['url'] } }` | Per-field control with nested options | ::: tip You rarely need to use `GetPayload` directly. The `StrapiClient` methods automatically infer the correct return type from your `populate` parameter. ::: ## Component Types Components are generated as separate interfaces, mirroring their Strapi structure. ```ts export interface SeoComponent { id: number metaTitle: string metaDescription: string | null shareImage: MediaFile | null } export interface SeoComponentInput { id?: number metaTitle?: string | null metaDescription?: string | null shareImage?: number | null } ``` Component types follow the same base/input split as content types. The `id` field is included (components have IDs in Strapi) but `documentId` is not (components are not standalone documents). ## PopulateParam Types For each content type and component that has populatable fields, a `PopulateParam` type is generated. This provides autocomplete and type safety for the `populate` parameter. ```ts export type ArticlePopulateParam = { category?: | true | { fields?: (keyof Category & string)[] populate?: | CategoryPopulateParam | (keyof CategoryPopulateParam & string)[] | '*' filters?: CategoryFilters sort?: SortValue | SortValue[] limit?: number start?: number } cover?: true | { fields?: (keyof MediaFile & string)[] } tags?: | true | { fields?: (keyof Tag & string)[] filters?: TagFilters sort?: SortValue | SortValue[] limit?: number start?: number } blocks?: | true | { on?: { 'shared.hero'?: true | { fields?: (keyof Hero & string)[] } 'shared.cta'?: true | { fields?: (keyof Cta & string)[] } } } } ``` Populate params support: * `true` for simple population. * Object syntax with `fields`, `filters`, `sort`, `limit`, `start` for fine-grained control. * Nested `populate` for deep population chains. * Dynamic zone `on` syntax for component-specific options. ## Filter Types Each content type gets a typed filter interface: ```ts export interface ArticleFilters extends LogicalOperators { id?: number | IdFilterOperators documentId?: string | StringFilterOperators title?: string | StringFilterOperators views?: number | NumberFilterOperators status?: ('draft' | 'published' | 'archived') | StringFilterOperators category?: { id?: number | IdFilterOperators documentId?: string | StringFilterOperators [key: string]: any } } ``` Filter utility types are also generated: | Type | Used For | | ------------------------ | --------------------------------------- | | `StringFilterOperators` | `$eq`, `$contains`, `$startsWith`, etc. | | `NumberFilterOperators` | `$eq`, `$lt`, `$gt`, `$between`, etc. | | `BooleanFilterOperators` | `$eq`, `$ne`, `$null` | | `DateFilterOperators` | `$eq`, `$lt`, `$gt`, `$between`, etc. | | `IdFilterOperators` | `$eq`, `$ne`, `$in`, `$notIn` | | `LogicalOperators` | `$and`, `$or`, `$not` | ## Internal Utility Types The following types are generated for internal use by the client and payload resolution. You generally do not need to reference them directly: | Type | Purpose | | ------------------------------------- | ---------------------------------------------------- | | `_EntityField` | Extracts field names excluding `__typename` | | `_SortValue` | Sort string like `'title'` or `'title:desc'` | | `_ApplyFields` | Narrows type based on `fields` selection in populate | | `Equal` | Exact type equality check | | `GetPopulated` | Maps base type to its `GetPayload` variant | | `SelectFields` | Picks specific fields from the type | ## TypeMap Generation The generated `StrapiClient` uses an internal `GetPopulated` conditional type that maps each base type to its `GetPayload` variant. This is what enables the client methods to automatically return the correct type based on the `populate` parameter you pass: ```ts type GetPopulated = Equal extends true ? ArticleGetPayload<{ populate: TPopulate }> : Equal extends true ? CategoryGetPayload<{ populate: TPopulate }> : // ... one branch per content type TBase ``` ::: info The `Equal` type uses exact equality instead of `extends` to prevent TypeScript structural typing from incorrectly matching similar types. ::: --- --- url: /strapi-typed-client/reference/media-file.md --- # Media & Blocks This page documents the `MediaFile` and `BlocksContent` types generated for Strapi media attributes and the Blocks rich text editor. ## MediaFile The `MediaFile` interface is generated for all Strapi media attributes. It represents a single uploaded file with its metadata. ```ts export interface MediaFormat { ext: string url: string hash: string mime: string name: string path: string | null size: number width: number height: number sizeInBytes: number } export interface MediaFile { id: number name: string alternativeText: string | null caption: string | null focalPoint: { x: number; y: number } | null width: number | null height: number | null formats: Record | null hash: string ext: string mime: string size: number url: string previewUrl: string | null provider: string createdAt: string updatedAt: string } ``` ### Field Reference | Field | Type | Description | | ----------------- | ------------------------------------- | ------------------------------------------------------------------------------ | | `id` | `number` | Database ID of the media entry | | `name` | `string` | Original file name | | `alternativeText` | `string \| null` | Alt text for accessibility | | `caption` | `string \| null` | Optional caption | | `focalPoint` | `{ x: number; y: number } \| null` | Upload focal point used for smart image positioning | | `width` | `number \| null` | Image width in pixels (null for non-images) | | `height` | `number \| null` | Image height in pixels (null for non-images) | | `formats` | `Record \| null` | Responsive image formats generated by Strapi (thumbnail, small, medium, large) | | `hash` | `string` | Unique hash of the file | | `ext` | `string` | File extension (e.g., `.jpg`, `.pdf`) | | `mime` | `string` | MIME type (e.g., `image/jpeg`, `application/pdf`) | | `size` | `number` | File size in kilobytes | | `url` | `string` | URL to access the file | | `previewUrl` | `string \| null` | Preview URL (used by some providers) | | `provider` | `string` | Storage provider (e.g., `local`, `aws-s3`, `cloudinary`) | | `createdAt` | `string` | ISO datetime of upload | | `updatedAt` | `string` | ISO datetime of last update | ### Single vs. Multiple Media Strapi media attributes can be configured as single or multiple: | Configuration | Base Type | Input Type | | --------------------------- | ------------------- | ----------------- | | Single media (required) | `MediaFile` | `MediaInput` | | Single media (not required) | `MediaFile \| null` | `MediaInput` | | Multiple media | `MediaFile[]` | `MultiMediaInput` | Where `MediaInput` is `StrapiID | null` and `MultiMediaInput` is `StrapiID[] | null` (`StrapiID = string | number`). ### Using Media with Populate Media fields require `populate` to be included in the response. Without populate, media fields are not present on the returned type. ```ts // Without populate — no media fields in the type const article = await strapi.articles.findOne('abc123') // article.cover <-- does not exist on type // With populate — media fields are included const article = await strapi.articles.findOne('abc123', { populate: { cover: true }, }) // article.cover?.url <-- fully typed as MediaFile | null ``` You can also select specific media fields: ```ts const article = await strapi.articles.findOne('abc123', { populate: { cover: { fields: ['url', 'alternativeText', 'width', 'height'] }, }, }) ``` ### Accessing Image URLs and Formats ```ts const article = await strapi.articles.findOne('abc123', { populate: { cover: true }, }) if (article.cover) { // Direct URL const imageUrl = article.cover.url // Alt text for tag const alt = article.cover.alternativeText ?? '' // Dimensions const { width, height } = article.cover // Responsive formats (thumbnail, small, medium, large) — fully typed if (article.cover.formats?.thumbnail) { const thumbnailUrl = article.cover.formats.thumbnail.url const { width, height } = article.cover.formats.thumbnail } } ``` ::: tip The `formats` field is typed as `Record | null`, covering Strapi's default formats (thumbnail, small, medium, large) and any custom sizes you configure. ::: ### Media in Input Types When creating or updating entries, media fields accept the file's numeric id (not file objects) — single media is typed `MediaInput`, multi media `MultiMediaInput`. Upload the file first via Strapi's upload API, then reference it by id. ```ts // Create with media by id await strapi.articles.create({ title: 'New Article', cover: 42, // single media -> MediaInput gallery: [42, 43], // multiple media -> MultiMediaInput }) // Clear a media field await strapi.articles.update('abc123', { cover: null, }) ``` ## Upload API Strapi v5 ships with a built-in upload plugin. The client exposes it via `client.upload`, fully typed and using the same auth pipeline, baseURL, error handling, and Next.js cache integration as the rest of the client. ### Methods | Method | Endpoint | Returns | | ------------------ | ------------------------------ | ------------- | | `upload(formData)` | `POST /api/upload` | `MediaFile[]` | | `find(params?)` | `GET /api/upload/files` | `MediaFile[]` | | `findOne(id)` | `GET /api/upload/files/:id` | `MediaFile` | | `destroy(id)` | `DELETE /api/upload/files/:id` | `MediaFile` | ### Uploading files Construct a `FormData` with the field name `files` (the Strapi convention) and pass it to `upload()`: ```ts // Web const input = document.querySelector('#picker')! const formData = new FormData() for (const file of input.files ?? []) { formData.append('files', file) } const uploaded = await client.upload.upload(formData) // uploaded is MediaFile[] ``` ```ts // React Native const formData = new FormData() formData.append('files', { uri: localUri, name: 'photo.jpg', type: 'image/jpeg', } as any) const [file] = await client.upload.upload(formData) ``` You can also attach uploaded files directly to an existing entry by adding the standard Strapi fields to the same FormData: ```ts formData.append('ref', 'api::article.article') formData.append('refId', String(articleId)) formData.append('field', 'cover') await client.upload.upload(formData) ``` ### Listing files `find()` accepts an `UploadQueryParams` shape with `filters`, `sort`, `fields`, and **flat `start`/`limit`** for pagination: ```ts const files = await client.upload.find({ sort: 'createdAt:desc', limit: 20, start: 0, filters: { mime: { $contains: 'image/' } }, }) ``` ::: warning Pagination quirk The upload plugin pre-dates Strapi v5's document model and **uses flat `start`/`limit` instead of `pagination[page]`/`pagination[pageSize]`**. The `UploadQueryParams` type reflects this — TypeScript will not let you pass `pagination` here. If you copy a snippet from a collection endpoint, you'll need to convert it. ::: ::: warning Response shape Unlike collection endpoints, the upload plugin returns a flat array — there is no `{ data, meta }` envelope. `client.upload.find()` resolves to `MediaFile[]` directly. ::: ### Reading and deleting a single file ```ts const file = await client.upload.findOne(42) console.log(file.url) const deleted = await client.upload.delete(42) // `deleted` is the MediaFile that was just removed ``` ::: warning Renamed in v2 `client.upload.destroy()` is now `client.upload.delete()`. The old name remains as a `@deprecated` alias and will be removed in a future major. ::: ::: warning Numeric ids Files use a numeric `id`, not a `documentId` — this is a quirk of the upload plugin (it pre-dates Strapi's document model). Do not pass strings: ```ts client.upload.findOne(42) // ✅ number client.upload.findOne('42') // ❌ type error ``` ::: ### Error handling Upload methods throw the same `StrapiError` / `StrapiConnectionError` as the rest of the client. Auth (Bearer token), timeout, and Next.js cache options are all inherited. ```ts try { await client.upload.delete(42) } catch (err) { if (err instanceof StrapiError && err.status === 403) { // missing permission to delete uploads } throw err } ``` ## BlocksContent The `BlocksContent` type represents content from Strapi's Blocks rich text editor (the v2 rich text field introduced in Strapi v5). It is a structured array of block objects. ```ts export type BlocksContent = Block[] ``` ### Block Types ```ts export type Block = | ParagraphBlock | HeadingBlock | QuoteBlock | CodeBlock | ListBlock | ImageBlock ``` #### ParagraphBlock ```ts export interface ParagraphBlock { type: 'paragraph' children: InlineNode[] } ``` #### HeadingBlock ```ts export interface HeadingBlock { type: 'heading' level: 1 | 2 | 3 | 4 | 5 | 6 children: InlineNode[] } ``` #### QuoteBlock ```ts export interface QuoteBlock { type: 'quote' children: InlineNode[] } ``` #### CodeBlock ```ts export interface CodeBlock { type: 'code' language?: string children: InlineNode[] } ``` #### ListBlock ```ts export interface ListBlock { type: 'list' format: 'ordered' | 'unordered' children: ListItemBlock[] } export interface ListItemBlock { type: 'list-item' children: InlineNode[] } ``` #### ImageBlock ```ts export interface ImageBlock { type: 'image' image: { name: string alternativeText?: string | null url: string caption?: string | null width?: number height?: number formats?: Record | null hash: string ext: string mime: string size: number previewUrl?: string | null provider: string createdAt: string updatedAt: string } children: InlineNode[] } ``` ### Inline Nodes All block types contain `children` arrays of inline nodes: ```ts export type InlineNode = TextNode | LinkInline ``` #### TextNode ```ts export interface TextNode { type: 'text' text: string bold?: boolean italic?: boolean underline?: boolean strikethrough?: boolean code?: boolean } ``` #### LinkInline ```ts export interface LinkInline { type: 'link' url: string children: TextNode[] } ``` ### Rendering Blocks on the Frontend `BlocksContent` is a structured data format. You need a renderer to convert it to HTML or React elements. Strapi provides an official renderer for React: ```bash npm install @strapi/blocks-react-renderer ``` ```tsx import { BlocksRenderer } from '@strapi/blocks-react-renderer' import type { BlocksContent } from '@/strapi' function ArticleBody({ content }: { content: BlocksContent }) { return } ``` ::: info The `BlocksContent` type is only generated when your schema contains at least one `blocks` type attribute. If your Strapi project uses only the classic `richtext` (Markdown) type, that field is typed as `string` instead. ::: ### Blocks vs. RichText | Attribute Type | Generated TypeScript Type | Format | | ------------------------ | ------------------------- | --------------------- | | `richtext` (Markdown) | `string` | Raw Markdown string | | `blocks` (Blocks editor) | `BlocksContent` | Structured JSON array | The `blocks` type provides a structured, renderable format, while `richtext` is a plain Markdown string that you parse and render yourself. --- --- url: /strapi-typed-client/reference/api.md --- # API Reference Complete reference for the `StrapiClient` class and all its methods. ## StrapiClient ### Constructor ```ts import { StrapiClient } from '@/strapi' const strapi = new StrapiClient(config: StrapiClientConfig) ``` **StrapiClientConfig:** | Property | Type | Required | Description | | ---------------- | -------------------- | -------- | ------------------------------------------------------------- | | `baseURL` | `string` | Yes | Strapi server URL (e.g., `http://localhost:1337`) | | `token` | `string` | No | Bearer token for authenticated requests | | `fetch` | `typeof fetch` | No | Custom fetch function (defaults to `globalThis.fetch`) | | `debug` | `boolean` | No | Log all requests to console | | `credentials` | `RequestCredentials` | No | Credentials mode for fetch (`include`, `same-origin`, `omit`) | | `timeout` | `number` | No | Request timeout in milliseconds. Aborts request if exceeded | | `validateSchema` | `boolean` | No | Check schema hash on init and warn if types are outdated | ```ts // Minimal configuration const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', }) // Full configuration const strapi = new StrapiClient({ baseURL: 'https://api.example.com', token: 'your-api-token', debug: process.env.NODE_ENV === 'development', credentials: 'include', validateSchema: process.env.NODE_ENV === 'development', }) // With custom fetch (e.g., for Node.js or testing) import fetch from 'node-fetch' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337', fetch: fetch as any, }) ``` ### setToken Updates the Bearer token used for all subsequent requests. ```ts strapi.setToken(token: string): void ``` ```ts // Set token after login const { jwt } = await strapi.auth.login({ identifier: 'user@example.com', password: 'password', }) strapi.setToken(jwt) ``` ### validateSchema Checks whether the locally generated types match the remote Strapi schema. ```ts strapi.validateSchema(): Promise<{ valid: boolean localHash: string remoteHash?: string error?: string }> ``` ```ts const result = await strapi.validateSchema() if (!result.valid) { console.warn('Types are outdated! Run: npx strapi-types generate') } ``` ::: tip When `validateSchema: true` is set in the config, this check runs automatically on client construction and logs a warning if the schema has drifted. ::: ## Collection API Each collection type gets a property on the `StrapiClient` instance. For example, if your Strapi project has an `Article` content type, you access it as `strapi.articles`. ### find Fetches a list of entries. ```ts find(params?, nextOptions?): Promise ``` **Parameters:** | Parameter | Type | Description | | ------------- | ------------- | -------------------------------------------------------------- | | `params` | `QueryParams` | Query parameters (filters, sort, pagination, populate, fields) | | `nextOptions` | `NextOptions` | Next.js cache options | **Returns:** `Promise` where `T` is the base type (or populated type if `populate` is specified). ```ts // Basic find — returns Article[] const articles = await strapi.articles.find() // With filters const published = await strapi.articles.find({ filters: { status: { $eq: 'published' } }, }) // With sort and pagination const recent = await strapi.articles.find({ sort: ['createdAt:desc'], pagination: { page: 1, pageSize: 10 }, }) // With populate — return type automatically includes relations const withCategory = await strapi.articles.find({ populate: { category: true, cover: true }, }) // withCategory[0].category <-- typed as Category | null // withCategory[0].cover <-- typed as MediaFile // Populate all relations const full = await strapi.articles.find({ populate: '*', }) // With field selection const titles = await strapi.articles.find({ fields: ['title', 'slug'], }) // With Next.js cache options const cached = await strapi.articles.find( { filters: { status: 'published' } }, { revalidate: 3600, tags: ['articles'] }, ) ``` ### findWithMeta Same as `find` but returns the full Strapi response including pagination metadata. ```ts findWithMeta(params?, nextOptions?): Promise> ``` **Returns:** `Promise>` with the response shape: ```ts interface StrapiResponse { data: T meta?: { pagination?: { page: number pageSize: number pageCount: number total: number } } } ``` ```ts const response = await strapi.articles.findWithMeta({ pagination: { page: 1, pageSize: 10 }, }) console.log(response.data) // Article[] console.log(response.meta.pagination) // { page: 1, pageSize: 10, pageCount: 5, total: 47 } ``` ### findOne Fetches a single entry by its document ID. ```ts findOne(documentId, params?, nextOptions?): Promise ``` **Parameters:** | Parameter | Type | Description | | ------------- | ------------- | ----------------------------------- | | `documentId` | `string` | The Strapi document ID | | `params` | `QueryParams` | Query parameters (populate, fields) | | `nextOptions` | `NextOptions` | Next.js cache options | **Returns:** `Promise` ```ts // Basic findOne const article = await strapi.articles.findOne('abc123') // With populate const article = await strapi.articles.findOne('abc123', { populate: { category: true, cover: { fields: ['url', 'alternativeText'] }, tags: { sort: ['name:asc'], limit: 5 }, }, }) // With Next.js options const article = await strapi.articles.findOne( 'abc123', { populate: { category: true } }, { revalidate: 60, tags: ['article-abc123'] }, ) ``` ### create Creates a new entry. ```ts create(data, nextOptions?): Promise ``` **Parameters:** | Parameter | Type | Description | | ------------- | -------------------- | --------------------------------------------------------- | | `data` | `TInput \| FormData` | The entry data (typed input or FormData for file uploads) | | `nextOptions` | `NextOptions` | Next.js cache options | **Returns:** `Promise` -- the created entry. ```ts // Create with typed input const article = await strapi.articles.create({ title: 'New Article', slug: 'new-article', body: 'Article content here.', status: 'draft', category: 5, // relation by ID cover: 12, // media by ID tags: [1, 2, 3], // many relation by IDs }) // Create with FormData (for file uploads in the same request) const formData = new FormData() formData.append('data', JSON.stringify({ title: 'With File' })) formData.append('files.cover', fileBlob, 'cover.jpg') const article = await strapi.articles.create(formData) ``` ### update Updates an existing entry by its document ID. ```ts update(documentId, data, nextOptions?): Promise ``` **Parameters:** | Parameter | Type | Description | | ------------- | -------------------- | ---------------------- | | `documentId` | `string` | The Strapi document ID | | `data` | `TInput \| FormData` | Partial entry data | | `nextOptions` | `NextOptions` | Next.js cache options | **Returns:** `Promise` -- the updated entry. ```ts // Partial update — only specified fields are changed const updated = await strapi.articles.update('abc123', { title: 'Updated Title', status: 'published', }) // Clear a field by setting it to null const cleared = await strapi.articles.update('abc123', { cover: null, body: null, }) ``` ### delete Deletes an entry by its document ID. ```ts delete(documentId, nextOptions?): Promise ``` **Parameters:** | Parameter | Type | Description | | ------------- | ------------- | ---------------------- | | `documentId` | `string` | The Strapi document ID | | `nextOptions` | `NextOptions` | Next.js cache options | **Returns:** `Promise` -- the deleted entry, or null. ```ts const deleted = await strapi.articles.delete('abc123') ``` ## Single Type API Single types (e.g., a Homepage or Global Settings content type) expose a reduced API since there is only one entry. ### find Fetches the single type entry. ```ts find(params?, nextOptions?): Promise ``` ```ts // Fetch the homepage single type const homepage = await strapi.homepage.find() // With populate const homepage = await strapi.homepage.find({ populate: { hero: true, seo: true }, }) ``` ### update Updates the single type entry. No `documentId` is needed. ```ts update(data, nextOptions?): Promise ``` ```ts const updated = await strapi.homepage.update({ heroTitle: 'Welcome to our site', }) ``` ::: info Single types do not have `findOne`, `create`, or `delete` methods. Use `find` to read and `update` to modify. ::: ## Authentication API The authentication API is available at `strapi.auth` and provides methods for the Strapi Users & Permissions plugin. ::: tip The exact methods available on `strapi.auth` depend on your Strapi project's auth and user controller routes. Common methods like `login`, `register`, `me`, and `forgotPassword` are generated automatically when the corresponding routes exist. ::: ::: warning Renamed in v2 `strapi.authentication` is now `strapi.auth`, and the client-side `logout()` helper is now `clearToken()`. The old names remain as `@deprecated` aliases and will be removed in a future major. ::: ## QueryParams The `QueryParams` interface is used by `find`, `findWithMeta`, and `findOne`. ```ts interface QueryParams { filters?: TFilters sort?: SortOption | SortOption[] pagination?: { page?: number pageSize?: number limit?: number start?: number } populate?: TPopulate fields?: TFields[] locale?: string status?: 'draft' | 'published' } ``` ### filters Type-safe filter object. Each content type gets its own `Filters` type. ```ts await strapi.articles.find({ filters: { title: { $contains: 'typescript' }, views: { $gte: 100 }, status: { $eq: 'published' }, $or: [ { category: { id: { $eq: 1 } } }, { category: { id: { $eq: 2 } } }, ], }, }) ``` **Available filter operators:** | Operator | Types | Description | | -------------- | ------------ | ------------------------------------ | | `$eq` | all | Equal | | `$eqi` | string | Equal (case-insensitive) | | `$ne` | all | Not equal | | `$lt`, `$lte` | number, date | Less than / less than or equal | | `$gt`, `$gte` | number, date | Greater than / greater than or equal | | `$in` | all | Included in array | | `$notIn` | all | Not included in array | | `$contains` | string | Contains substring | | `$notContains` | string | Does not contain substring | | `$containsi` | string | Contains (case-insensitive) | | `$startsWith` | string | Starts with | | `$endsWith` | string | Ends with | | `$between` | number, date | Between two values | | `$null` | all | Is null | | `$notNull` | all | Is not null | | `$and` | logical | All conditions must match | | `$or` | logical | At least one condition must match | | `$not` | logical | Negation | ### sort Sort by one or more fields. Append `:asc` or `:desc` for direction. ```ts await strapi.articles.find({ sort: ['createdAt:desc'], }) await strapi.articles.find({ sort: ['status:asc', 'createdAt:desc'], }) ``` ### pagination Two pagination styles are supported: ```ts // Page-based pagination await strapi.articles.find({ pagination: { page: 2, pageSize: 25 }, }) // Offset-based pagination await strapi.articles.find({ pagination: { start: 50, limit: 25 }, }) ``` ### populate See [Populate & Type Inference](/guide/populate) for a detailed guide. Quick summary: ```ts // Populate specific relations { populate: { category: true, cover: true } } // Populate all relations (1 level deep) { populate: '*' } // Nested populate with options { populate: { category: { fields: ['name', 'slug'], populate: { icon: true } } }} ``` ### fields Select specific fields to return. Reduces payload size. ```ts await strapi.articles.find({ fields: ['title', 'slug', 'createdAt'], }) ``` ### locale Filter by locale for content types with the Strapi i18n plugin enabled. ```ts // Fetch content in a specific locale await strapi.articles.find({ locale: 'en', }) // Combine with populate to get localizations await strapi.articles.find({ locale: 'en', populate: { localizations: { fields: ['locale', 'slug'] }, }, }) ``` ::: info The `locale` parameter is available on all content types. Strapi ignores it for content types without i18n enabled. ::: ### status Filter by publication status. Useful for preview/editor flows where you need to access draft content. ```ts // Fetch only draft entries await strapi.articles.find({ status: 'draft', }) // Fetch only published entries (default Strapi behavior) await strapi.articles.find({ status: 'published', }) ``` ::: info See the [Strapi documentation on status](https://docs.strapi.io/cms/api/rest/status) for more details. By default, Strapi returns only published entries when `status` is omitted. ::: ## NextOptions Optional second parameter on all API methods for Next.js cache control. ```ts interface NextOptions { revalidate?: number | false tags?: string[] cache?: RequestCache headers?: Record } ``` | Property | Type | Description | | ------------ | ------------------------------------- | --------------------------------------------------------------- | | `revalidate` | `number \| false` | ISR revalidation period in seconds, or `false` to disable | | `tags` | `string[]` | Cache tags for on-demand revalidation via `revalidateTag()` | | `cache` | `RequestCache` | Standard fetch cache mode (`'no-store'`, `'force-cache'`, etc.) | | `headers` | `Record` | Custom HTTP headers to merge into the request | ```ts // ISR: revalidate every hour await strapi.articles.find({}, { revalidate: 3600 }) // Tag-based revalidation await strapi.articles.find({}, { tags: ['articles'] }) // No caching await strapi.articles.find({}, { cache: 'no-store' }) // Custom headers (e.g. pass Referer for server-side requests) await strapi.articles.find({}, { headers: { Referer: 'https://myapp.com' } }) // Combine options await strapi.articles.find( { filters: { status: 'published' } }, { revalidate: 300, tags: ['articles', 'published'] }, ) ``` ::: info `NextOptions` are passed through to the underlying `fetch` call as `{ next: { revalidate, tags }, cache }`. Custom `headers` are merged with the default headers (Content-Type, Authorization). These options work in any environment, not just Next.js. ::: ## StrapiResponse The response wrapper returned by `findWithMeta`. ```ts interface StrapiResponse { data: T meta?: { pagination?: { page: number pageSize: number pageCount: number total: number } } } ``` ## StrapiError Custom error class thrown on non-OK HTTP responses. ```ts class StrapiError extends Error { /** Clean user-friendly message from Strapi backend */ userMessage: string /** HTTP status code */ status: number /** HTTP status text */ statusText: string /** * Strapi-side error name (e.g. "ValidationError", "PolicyError"). * Use as discriminator with `isStrapiErrorOf` for typed `details`. */ errorName: StrapiErrorName /** Narrow via `isStrapiErrorOf` for typed access. */ details?: unknown } ``` ::: warning Note on `Error.name` `Error.name` is left as `"StrapiError"` so Sentry/sourcemap contracts are unchanged — the Strapi-side name lives on the new `errorName` field. Use `errorName` (not `name`) for narrowing. ::: The error message also includes a contextual hint for common HTTP status codes: | Status | Hint | | ------ | ------------------------------------------------------------------------------------ | | 401 | Check that your API token is valid and passed to StrapiClient config. | | 403 | Your token may lack permissions for this endpoint. Check Strapi roles & permissions. | | 404 | This endpoint may not exist. Verify the content type is created in Strapi. | | 500 | Internal Strapi error. Check Strapi server logs for details. | ### Known error names `errorName` is typed as a literal union of Strapi v5's documented error names plus a `string` fallback for plugin or future-version errors: | `errorName` | Default status | `details` shape | | ---------------------- | -------------- | --------------------------------------- | | `ValidationError` | 400 | `{ errors: StrapiValidationIssue[] }` | | `BadRequestError` | 400 | `Record` | | `PaginationError` | 400 | `Record` | | `UnauthorizedError` | 401 | `undefined` | | `ForbiddenError` | 403 | `undefined` | | `PolicyError` | 403 | `{ policy?: string; message?: string }` | | `NotFoundError` | 404 | `undefined` | | `ConflictError` | 409 | `Record` | | `PayloadTooLargeError` | 413 | `undefined` | | `RateLimitError` | 429 | `Record` | | `ApplicationError` | 500 | `Record` | ### Narrowing errors with `isStrapiErrorOf` Because `details` is typed as `unknown` on the class, narrow it via the `isStrapiErrorOf` type guard: ```ts import { isStrapiErrorOf } from '@/strapi' try { await strapi.articles.create({ title: '' }) } catch (err) { if (isStrapiErrorOf(err, 'ValidationError')) { // err.details is `{ errors: StrapiValidationIssue[] } | undefined` — // Strapi can omit details even on a ValidationError, so use // optional access or a fallback. for (const issue of err.details?.errors ?? []) { console.log(issue.path.join('.'), issue.message) } } if (isStrapiErrorOf(err, 'PolicyError')) { console.warn(err.details?.policy) } if (isStrapiErrorOf(err, 'ForbiddenError')) { // 403 — token lacks permission. err.details is always undefined. redirectToLogin() } } ``` For "any Strapi error" without a specific name, use `isStrapiError`: ```ts import { isStrapiError } from '@/strapi' try { /* ... */ } catch (err) { if (isStrapiError(err)) { if (err.status === 401) redirectToLogin() } } ``` ### `StrapiValidationIssue` Each entry in `details.errors` for a `ValidationError`: ```ts export interface StrapiValidationIssue { /** Field path to the offending value, e.g. ["author", "email"]. */ path: string[] message: string /** The Yup validator name, e.g. "required". */ name: string } ``` This is a stable export — perfect for mapping backend validation back to form fields: ```ts const fieldErrors: Record = {} if (isStrapiErrorOf(err, 'ValidationError')) { for (const issue of err.details?.errors ?? []) { fieldErrors[issue.path.join('.')] = issue.message } } ``` ### Migration from `details: any` Earlier versions exposed `details` as `any`. The new `unknown` type is stricter — direct property access stops compiling without narrowing: ```ts // Before — compiled but unsafe: err.details.errors[0].path // After — must narrow: if (isStrapiErrorOf(err, 'ValidationError')) { err.details.errors[0].path } ``` Runtime behavior is unchanged. ## StrapiConnectionError Error thrown when the client cannot reach the Strapi server at all (network failures, DNS errors, timeouts). ```ts class StrapiConnectionError extends Error { /** The URL that was being requested */ url: string /** The original error that caused the connection failure */ cause?: Error } ``` The error message is specific to the failure type: | Cause | Message | | -------------------- | ------------------------------------------------------------------ | | Server not running | `Could not connect to Strapi at {baseURL}. Is the server running?` | | DNS resolution error | `Could not resolve host. Check your baseURL: {baseURL}` | | Request timeout | `Request timed out after {timeout}ms. URL: {url}` | | Other network error | `Network error: {message}. Check your baseURL: {baseURL}` | ```ts import { StrapiConnectionError } from '@/strapi' try { await strapi.articles.find() } catch (error) { if (error instanceof StrapiConnectionError) { console.log(error.message) // "Could not connect to Strapi at http://localhost:1337. Is the server running?" console.log(error.url) // the full request URL console.log(error.cause) // original fetch error } } ```