DEV Community

Dearonski
Dearonski

Posted on

Stop Writing Strapi Types by Hand — Auto-Generate a Fully Typed Client in Seconds

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The solution

Install the package in your Strapi project:

npm install strapi-typed-client
Enter fullscreen mode Exit fullscreen mode

Enable the plugin:

// config/plugins.ts
export default {
  'strapi-typed-client': { enabled: true },
}
Enter fullscreen mode Exit fullscreen mode

Generate types from your running Strapi instance:

npx strapi-types generate --url http://localhost:1337
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Nested populate works too, with unlimited depth:

const article = await strapi.articles.findOne('abc123', {
  populate: {
    category: {
      populate: { articles: true },
    },
    author: {
      fields: ['name', 'email'],
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Components & Dynamic Zones

Components generate as separate interfaces:

export interface Seo {
  id: number
  metaTitle: string
  metaDescription: string
  ogImage: MediaFile | null
  canonicalUrl: string | null
}
Enter fullscreen mode Exit fullscreen mode

Dynamic Zones become union types:

export type PageContentDynamicZone = HeroSection | FeatureGrid | Testimonial
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'] }
)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode
  • 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 } },
    ],
  },
})
Enter fullscreen mode Exit fullscreen mode

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' })
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Try it

npm install strapi-typed-client
npx strapi-types generate --url http://localhost:1337
Enter fullscreen mode Exit fullscreen mode

The package is already powering a large production project and is actively maintained. Feedback and contributions welcome!

Top comments (0)