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

# typescript# nextjs# webdev# opensource
Stop Writing Strapi Types by Hand — Auto-Generate a Fully Typed Client in SecondsDearonski

If 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.

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!