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.
The problem
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
The solution
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:
Clean TypeScript interfaces
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.
A typed API client
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
Type-safe populate
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 & Dynamic Zones
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
RichText blocks — typed, no React dependency
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.
Next.js integration
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 fly -
next build— one-time generation before build - Zero manual steps
Entity-specific filters
Every 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 } },
],
},
})
Custom API endpoints
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' })
Schema hashing
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
Try it
npm install strapi-typed-client
npx strapi-types generate --url http://localhost:1337
- GitHub: BoxLab-Ltd/strapi-typed-client
- Docs: boxlab-ltd.github.io/strapi-typed-client
- npm: strapi-typed-client
The package is already powering a large production project and is actively maintained. Feedback and contributions welcome!
Top comments (0)