DearonskiIf you're using Strapi v5 with TypeScript, you've probably spent hours writing interfaces to match...
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:
// 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
}
And then building fetch wrappers with zero type safety:
const res = await fetch(`${STRAPI_URL}/api/articles?populate=*`)
const data = await res.json()
// data is `any` — good luck
Install the package in your Strapi project:
npm install strapi-typed-client
Enable the plugin:
// config/plugins.ts
export default {
'strapi-typed-client': { enabled: true },
}
Generate types from your running Strapi instance:
npx strapi-types generate --url http://localhost:1337
That's it. You get two generated files:
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
}
No generics, no Schema.Attribute.* wrappers. Just plain TypeScript that your editor understands.
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
This is where it gets interesting. The generated types include Prisma-style GetPayload helpers:
// 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
Nested populate works too, with unlimited depth:
const article = await strapi.articles.findOne('abc123', {
populate: {
category: {
populate: { articles: true },
},
author: {
fields: ['name', 'email'],
},
},
})
Components generate as separate interfaces:
export interface Seo {
id: number
metaTitle: string
metaDescription: string
ogImage: MediaFile | null
canonicalUrl: string | null
}
Dynamic Zones become union types:
export type PageContentDynamicZone = HeroSection | FeatureGrid | Testimonial
Strapi's Blocks editor fields get proper types instead of plain string:
export type BlocksContent = Block[]
export type Block =
| ParagraphBlock
| HeadingBlock
| QuoteBlock
| CodeBlock
| ListBlock
| ImageBlock
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:
const articles = await strapi.articles.find(
{ populate: { category: true } },
{ revalidate: 3600, tags: ['articles'] }
)
There's also a withStrapiTypes wrapper that makes type generation fully automatic:
// next.config.ts
import { withStrapiTypes } from 'strapi-typed-client/next'
export default withStrapiTypes()(nextConfig)
next dev — polls Strapi for schema changes, regenerates types on the flynext build — one-time generation before buildEvery collection gets typed filter operators scoped to its field types:
// 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 } },
],
},
})
If you have custom controllers (not just CRUD), the plugin detects them and generates typed methods:
// Generated from your custom routes
await strapi.newsletter.subscribe({ email: 'user@example.com' })
await strapi.search.find({ q: 'typescript' })
The CLI computes a SHA-256 hash of your schema. If nothing changed since last run — generation is skipped. Fast CI, no unnecessary rebuilds.
# Check if types are up to date (useful in CI)
npx strapi-types check --url http://localhost:1337
npm install strapi-typed-client
npx strapi-types generate --url http://localhost:1337
The package is already powering a large production project and is actively maintained. Feedback and contributions welcome!