Tools
Tools: Stop Writing Strapi Types by Hand — Auto-Generate a Fully Typed Client in Seconds
2026-02-13
0 views
admin
The problem ## The solution ## Clean TypeScript interfaces ## A typed API client ## Type-safe populate ## Components & Dynamic Zones ## RichText blocks — typed, no React dependency ## Next.js integration ## Entity-specific filters ## Custom API endpoints ## Schema hashing ## Try it If you're using Strapi v5 with TypeScript, you've probably spent hours writing interfaces to match your content types. And every time you change a field in Strapi — you update the types manually. Again. I built strapi-typed-client to solve this. It's a Strapi plugin + CLI that reads your schema and generates clean TypeScript types and a fully typed API client. One command, full autocomplete. Strapi generates contentTypes.d.ts internally, but it's full of Schema.Attribute.* generics that are unusable on the frontend. You end up writing something like: And then building fetch wrappers with zero type safety: Install the package in your Strapi project: Generate types from your running Strapi instance: That's it. You get two generated files: No generics, no Schema.Attribute.* wrappers. Just plain TypeScript that your editor understands. This is where it gets interesting. The generated types include Prisma-style GetPayload helpers: Nested populate works too, with unlimited depth: Components generate as separate interfaces: Dynamic Zones become union types: Strapi's Blocks editor fields get proper types instead of plain string: Framework-agnostic — use with Vue, Svelte, Astro, anything. The client uses native fetch, so Next.js caching, deduplication, and ISR work out of the box: There's also a withStrapiTypes wrapper that makes type generation fully automatic: Every collection gets typed filter operators scoped to its field types: If you have custom controllers (not just CRUD), the plugin detects them and generates typed methods: The CLI computes a SHA-256 hash of your schema. If nothing changed since last run — generation is skipped. Fast CI, no unnecessary rebuilds. The package is already powering a large production project and is actively maintained. Feedback and contributions welcome! Templates let you quickly answer FAQs or store snippets for re-use. Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment's permalink. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse CODE_BLOCK:
// Writing this by hand for every content type...
interface Article { id: number title: string content: string category?: Category // did I forget a field? who knows
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Writing this by hand for every content type...
interface Article { id: number title: string content: string category?: Category // did I forget a field? who knows
} CODE_BLOCK:
// Writing this by hand for every content type...
interface Article { id: number title: string content: string category?: Category // did I forget a field? who knows
} CODE_BLOCK:
const res = await fetch(`${STRAPI_URL}/api/articles?populate=*`)
const data = await res.json()
// data is `any` — good luck Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const res = await fetch(`${STRAPI_URL}/api/articles?populate=*`)
const data = await res.json()
// data is `any` — good luck CODE_BLOCK:
const res = await fetch(`${STRAPI_URL}/api/articles?populate=*`)
const data = await res.json()
// data is `any` — good luck COMMAND_BLOCK:
npm install strapi-typed-client Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
npm install strapi-typed-client COMMAND_BLOCK:
npm install strapi-typed-client CODE_BLOCK:
// config/plugins.ts
export default { 'strapi-typed-client': { enabled: true },
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// config/plugins.ts
export default { 'strapi-typed-client': { enabled: true },
} CODE_BLOCK:
// config/plugins.ts
export default { 'strapi-typed-client': { enabled: true },
} CODE_BLOCK:
npx strapi-types generate --url http://localhost:1337 Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
npx strapi-types generate --url http://localhost:1337 CODE_BLOCK:
npx strapi-types generate --url http://localhost:1337 CODE_BLOCK:
export interface Article { id: number documentId: string title: string slug: string content: BlocksContent excerpt: string | null cover: MediaFile | null category: Category | null author: Author | null tags: Tag[] publishedDate: string | null featured: boolean seo: Seo | null createdAt: string updatedAt: string
} export interface ArticleInput { title: string slug?: string content?: BlocksContent excerpt?: string | null cover?: number | null // relations as IDs for create/update category?: number | null author?: number | null tags?: number[] publishedDate?: string | null featured?: boolean
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
export interface Article { id: number documentId: string title: string slug: string content: BlocksContent excerpt: string | null cover: MediaFile | null category: Category | null author: Author | null tags: Tag[] publishedDate: string | null featured: boolean seo: Seo | null createdAt: string updatedAt: string
} export interface ArticleInput { title: string slug?: string content?: BlocksContent excerpt?: string | null cover?: number | null // relations as IDs for create/update category?: number | null author?: number | null tags?: number[] publishedDate?: string | null featured?: boolean
} CODE_BLOCK:
export interface Article { id: number documentId: string title: string slug: string content: BlocksContent excerpt: string | null cover: MediaFile | null category: Category | null author: Author | null tags: Tag[] publishedDate: string | null featured: boolean seo: Seo | null createdAt: string updatedAt: string
} export interface ArticleInput { title: string slug?: string content?: BlocksContent excerpt?: string | null cover?: number | null // relations as IDs for create/update category?: number | null author?: number | null tags?: number[] publishedDate?: string | null featured?: boolean
} CODE_BLOCK:
import { StrapiClient } from 'strapi-typed-client' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337',
}) // Full autocomplete on collection names, filter fields, sort options
const articles = await strapi.articles.find({ filters: { title: { $contains: 'hello' } }, populate: { category: true, author: true, tags: true }, sort: ['publishedDate:desc'], pagination: { page: 1, pageSize: 10 },
}) // articles[0].category.name — fully typed, no casting Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
import { StrapiClient } from 'strapi-typed-client' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337',
}) // Full autocomplete on collection names, filter fields, sort options
const articles = await strapi.articles.find({ filters: { title: { $contains: 'hello' } }, populate: { category: true, author: true, tags: true }, sort: ['publishedDate:desc'], pagination: { page: 1, pageSize: 10 },
}) // articles[0].category.name — fully typed, no casting CODE_BLOCK:
import { StrapiClient } from 'strapi-typed-client' const strapi = new StrapiClient({ baseURL: 'http://localhost:1337',
}) // Full autocomplete on collection names, filter fields, sort options
const articles = await strapi.articles.find({ filters: { title: { $contains: 'hello' } }, populate: { category: true, author: true, tags: true }, sort: ['publishedDate:desc'], pagination: { page: 1, pageSize: 10 },
}) // articles[0].category.name — fully typed, no casting CODE_BLOCK:
// Without populate — relations are { id, documentId }
const article = await strapi.articles.findOne('abc123')
article.category // { id: number, documentId: string } | null // With populate — relations expand to full types
const article = await strapi.articles.findOne('abc123', { populate: { category: true, author: true },
})
article.category // Category | null (with all fields)
article.author // Author | null Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Without populate — relations are { id, documentId }
const article = await strapi.articles.findOne('abc123')
article.category // { id: number, documentId: string } | null // With populate — relations expand to full types
const article = await strapi.articles.findOne('abc123', { populate: { category: true, author: true },
})
article.category // Category | null (with all fields)
article.author // Author | null CODE_BLOCK:
// Without populate — relations are { id, documentId }
const article = await strapi.articles.findOne('abc123')
article.category // { id: number, documentId: string } | null // With populate — relations expand to full types
const article = await strapi.articles.findOne('abc123', { populate: { category: true, author: true },
})
article.category // Category | null (with all fields)
article.author // Author | null CODE_BLOCK:
const article = await strapi.articles.findOne('abc123', { populate: { category: { populate: { articles: true }, }, author: { fields: ['name', 'email'], }, },
}) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const article = await strapi.articles.findOne('abc123', { populate: { category: { populate: { articles: true }, }, author: { fields: ['name', 'email'], }, },
}) CODE_BLOCK:
const article = await strapi.articles.findOne('abc123', { populate: { category: { populate: { articles: true }, }, author: { fields: ['name', 'email'], }, },
}) CODE_BLOCK:
export interface Seo { id: number metaTitle: string metaDescription: string ogImage: MediaFile | null canonicalUrl: string | null
} Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
export interface Seo { id: number metaTitle: string metaDescription: string ogImage: MediaFile | null canonicalUrl: string | null
} CODE_BLOCK:
export interface Seo { id: number metaTitle: string metaDescription: string ogImage: MediaFile | null canonicalUrl: string | null
} CODE_BLOCK:
export type PageContentDynamicZone = HeroSection | FeatureGrid | Testimonial Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
export type PageContentDynamicZone = HeroSection | FeatureGrid | Testimonial CODE_BLOCK:
export type PageContentDynamicZone = HeroSection | FeatureGrid | Testimonial CODE_BLOCK:
export type BlocksContent = Block[] export type Block = | ParagraphBlock | HeadingBlock | QuoteBlock | CodeBlock | ListBlock | ImageBlock Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
export type BlocksContent = Block[] export type Block = | ParagraphBlock | HeadingBlock | QuoteBlock | CodeBlock | ListBlock | ImageBlock CODE_BLOCK:
export type BlocksContent = Block[] export type Block = | ParagraphBlock | HeadingBlock | QuoteBlock | CodeBlock | ListBlock | ImageBlock CODE_BLOCK:
const articles = await strapi.articles.find( { populate: { category: true } }, { revalidate: 3600, tags: ['articles'] }
) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
const articles = await strapi.articles.find( { populate: { category: true } }, { revalidate: 3600, tags: ['articles'] }
) CODE_BLOCK:
const articles = await strapi.articles.find( { populate: { category: true } }, { revalidate: 3600, tags: ['articles'] }
) CODE_BLOCK:
// next.config.ts
import { withStrapiTypes } from 'strapi-typed-client/next' export default withStrapiTypes()(nextConfig) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// next.config.ts
import { withStrapiTypes } from 'strapi-typed-client/next' export default withStrapiTypes()(nextConfig) CODE_BLOCK:
// next.config.ts
import { withStrapiTypes } from 'strapi-typed-client/next' export default withStrapiTypes()(nextConfig) CODE_BLOCK:
// Only valid filter operators for each field type
await strapi.articles.find({ filters: { title: { $contains: 'hello' }, // string operators readTime: { $gte: 5 }, // number operators featured: { $eq: true }, // boolean publishedDate: { $gte: '2025-01-01' }, // date as string category: { name: { $eq: 'Tech' } }, // nested relation filters $or: [ { featured: { $eq: true } }, { readTime: { $gte: 10 } }, ], },
}) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Only valid filter operators for each field type
await strapi.articles.find({ filters: { title: { $contains: 'hello' }, // string operators readTime: { $gte: 5 }, // number operators featured: { $eq: true }, // boolean publishedDate: { $gte: '2025-01-01' }, // date as string category: { name: { $eq: 'Tech' } }, // nested relation filters $or: [ { featured: { $eq: true } }, { readTime: { $gte: 10 } }, ], },
}) CODE_BLOCK:
// Only valid filter operators for each field type
await strapi.articles.find({ filters: { title: { $contains: 'hello' }, // string operators readTime: { $gte: 5 }, // number operators featured: { $eq: true }, // boolean publishedDate: { $gte: '2025-01-01' }, // date as string category: { name: { $eq: 'Tech' } }, // nested relation filters $or: [ { featured: { $eq: true } }, { readTime: { $gte: 10 } }, ], },
}) CODE_BLOCK:
// Generated from your custom routes
await strapi.newsletter.subscribe({ email: '[email protected]' })
await strapi.search.find({ q: 'typescript' }) Enter fullscreen mode Exit fullscreen mode CODE_BLOCK:
// Generated from your custom routes
await strapi.newsletter.subscribe({ email: '[email protected]' })
await strapi.search.find({ q: 'typescript' }) CODE_BLOCK:
// Generated from your custom routes
await strapi.newsletter.subscribe({ email: '[email protected]' })
await strapi.search.find({ q: 'typescript' }) COMMAND_BLOCK:
# Check if types are up to date (useful in CI)
npx strapi-types check --url http://localhost:1337 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
# Check if types are up to date (useful in CI)
npx strapi-types check --url http://localhost:1337 COMMAND_BLOCK:
# Check if types are up to date (useful in CI)
npx strapi-types check --url http://localhost:1337 COMMAND_BLOCK:
npm install strapi-typed-client
npx strapi-types generate --url http://localhost:1337 Enter fullscreen mode Exit fullscreen mode COMMAND_BLOCK:
npm install strapi-typed-client
npx strapi-types generate --url http://localhost:1337 COMMAND_BLOCK:
npm install strapi-typed-client
npx strapi-types generate --url http://localhost:1337 - next dev — polls Strapi for schema changes, regenerates types on the fly
- next build — one-time generation before build
- Zero manual steps - GitHub: BoxLab-Ltd/strapi-typed-client
- Docs: boxlab-ltd.github.io/strapi-typed-client
- npm: strapi-typed-client
how-totutorialguidedev.toaigitgithub