Strapi with TypeScript — TypeScript Setup, Type Generation, and Strict Mode
In this tutorial, you will learn how to use TypeScript with Strapi — setting up a TypeScript project, leveraging automatic type generation from your content types, writing type-safe services and controllers, and configuring strict mode for better type checking.
What You'll Learn
- How to create a Strapi project with TypeScript
- How automatic type generation works for content types
- How to use generated types in controllers and services
- How to create custom types for plugins and extensions
- How to configure TypeScript strict mode
- Benefits of TypeScript for Strapi development
Why It Matters
TypeScript catches errors at compile time that would otherwise only be found at runtime. In a Strapi project, TypeScript ensures you are accessing the correct fields on your content types, calling functions with the right arguments, and handling responses properly. For team projects, TypeScript provides self-documenting code that makes onboarding faster and reduces production bugs.
Real-World Use
A team of 5 developers maintains a Strapi project with 15 content types, 8 custom plugins, and dozens of lifecycle hooks. Before TypeScript, a missed field name in a controller caused a production outage when users created articles without the "category" field. With TypeScript, the compiler catches the error during development. The team estimates TypeScript prevented 30+ production bugs in the first quarter.
Learning Path
flowchart LR A["Webhooks"] --> B["Strapi TypeScript
-- You are here"]:::current B --> C["Testing"] classDef current fill:#4945ff,color:#fff,stroke-width:2px
Creating a TypeScript Project
You can start a new Strapi project with TypeScript from the beginning:
# Create with TypeScript
npx create-strapi-app@latest my-project --typescript
# Or add TypeScript to an existing JavaScript project
npm install typescript @types/node --save-dev
npx tsc --init
A TypeScript Strapi project has this structure:
my-project/
src/
api/
article/
controllers/
article.ts -- TypeScript file
services/
article.ts
routes/
article.ts
content-types/
article/
schema.json
config/
admin.ts
database.ts
server.ts
middlewares.ts
plugins.ts
tsconfig.json
package.json
The key difference from JavaScript: .ts files replace .js files, and a tsconfig.json configures the TypeScript compiler.
Automatic Type Generation
Strapi can generate TypeScript types from your content type schemas:
# Generate types for all content types
npm run strapi generate:types
# This creates:
# types/generated/
# contentTypes.d.ts -- Types for all content types
# components.d.ts -- Types for all components
The generated types look like this:
// types/generated/contentTypes.d.ts
export interface Article {
id: number;
title: string;
content: string;
slug: string;
publishedAt: string | null;
createdAt: string;
updatedAt: string;
author?: Author;
tags?: Tag[];
}
export interface Author {
id: number;
name: string;
email: string;
bio?: string;
avatar?: any; // Media field
}
export interface Tag {
id: number;
name: string;
slug: string;
}
Use these types in your code for type safety:
// src/api/article/controllers/article.ts
import { factories } from "@strapi/strapi";
import { Article } from "../../../../types/generated/contentTypes";
export default factories.createCoreController("api::article.article", ({ strapi }) => ({
async find(ctx) {
// ctx.query is typed
const { data, meta } = await super.find(ctx);
// data is typed as Article[]
const publishedArticles = (data as Article[]).filter(
(article) => article.publishedAt !== null
);
return { data: publishedArticles, meta };
},
async create(ctx) {
// ctx.request.body is typed based on the schema
const { data } = ctx.request.body;
// TypeScript ensures 'title' exists on the Article type
if (!data.title || data.title.length < 3) {
return ctx.badRequest("Title must be at least 3 characters");
}
return super.create(ctx);
},
}));
Type-Safe Services
// src/api/article/services/article.ts
import { factories } from "@strapi/strapi";
import { Article } from "../../../../types/generated/contentTypes";
export default factories.createCoreService("api::article.article", ({ strapi }) => ({
async findPublishedArticles(): Promise<Article[]> {
const articles = await strapi.entityService.findMany("api::article.article", {
filters: { publishedAt: { $notNull: true } },
populate: ["author", "tags"],
});
return articles as Article[];
},
async getArticleCountByAuthor(authorId: number): Promise<number> {
const count = await strapi.db.query("api::article.article").count({
where: { author: authorId },
});
return count;
},
async validateArticleData(data: Partial<Article>): Promise<boolean> {
if (!data.title || data.title.length < 1) {
throw new Error("Title is required");
}
if (data.title && data.title.length > 200) {
throw new Error("Title must be less than 200 characters");
}
return true;
},
}));
Custom Type Definitions
For types not covered by generation, create custom type files:
// types/custom/index.d.ts
export interface WebhookPayload {
event: string;
createdAt: string;
model: string;
entry: Record<string, unknown>;
}
export interface ApiResponse<T> {
data: T | T[];
meta?: {
pagination?: {
page: number;
pageSize: number;
pageCount: number;
total: number;
};
};
}
export interface PaginationParams {
page?: number;
pageSize?: number;
start?: number;
limit?: number;
}
export interface FilterParams {
[key: string]: {
$eq?: string | number;
$ne?: string | number;
$gt?: number;
$gte?: number;
$lt?: number;
$lte?: number;
$in?: (string | number)[];
$notIn?: (string | number)[];
$contains?: string;
$containsi?: string;
$startsWith?: string;
$endsWith?: string;
$null?: boolean;
$notNull?: boolean;
};
}
Strict Mode Configuration
Configure TypeScript strict mode for maximum safety:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"paths": {
"@strapi/strapi": ["./node_modules/@strapi/strapi"],
"@strapi/utils": ["./node_modules/@strapi/utils"]
}
},
"include": ["src/**/*.ts", "types/**/*.d.ts"],
"exclude": ["node_modules", "dist"]
}
With strict: true, TypeScript enables:
strictNullChecks— null and undefined are not assignable to other typesnoImplicitAny— must declare types explicitlynoImplicitThis— must typethisin functionsalwaysStrict— JavaScript strict mode is always enabled
TypeScript with Lifecycle Hooks
// src/api/article/content-types/article/lifecycle.ts
import { Strapi } from "@strapi/strapi";
interface LifecycleEvent {
action: string;
model: { uid: string };
params: {
data?: Record<string, unknown>;
where?: Record<string, unknown>;
};
result?: Record<string, unknown>;
state: Record<string, unknown>;
}
export default {
beforeCreate(event: LifecycleEvent) {
const { data } = event.params;
if (data && typeof data.title === "string") {
// TypeScript knows data.title is a string
data.title = data.title.trim();
}
},
afterCreate: async (event: LifecycleEvent) => {
const { result } = event;
if (result) {
strapi.log.info(`Article created: ${result.id}`);
}
},
};
TypeScript with Middleware
// src/middlewares/request-logger.ts
import { Strapi } from "@strapi/strapi";
import { ParameterizedContext } from "koa";
interface MiddlewareConfig {
logHeaders?: boolean;
logBody?: boolean;
}
export default (config: MiddlewareConfig, { strapi }: { strapi: Strapi }) => {
return async (ctx: ParameterizedContext, next: () => Promise<void>) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
const logData: Record<string, unknown> = {
method: ctx.method,
url: ctx.url,
status: ctx.status,
duration: `${duration}ms`,
};
if (config.logHeaders) {
logData.headers = ctx.request.headers;
}
strapi.log.info(JSON.stringify(logData));
};
};
Common Mistakes
Using
anyeverywhere. TypeScript's real benefit is catching type errors. Usinganydefeats this. Define proper types for your data structures.Not regenerating types after schema changes. When you add fields or change content types, the generated types become outdated. Run
npm run strapi generate:typesafter every schema change.Mixing JavaScript and TypeScript. While Strapi supports mixed projects, it is best to be consistent. Choose one language per project. If you start with TypeScript, write all custom code in TypeScript.
Ignoring TypeScript compilation errors. TypeScript errors are warnings by default in many setups. Configure your build to fail on TypeScript errors to prevent type-unsafe code from reaching production.
Not typing API responses and requests. The
ctx.request.bodyand API responses benefit significantly from proper typing. Define interfaces for your request and response shapes.
Practice Questions
How do you create a Strapi project with TypeScript? Answer: Use
npx create-strapi-app@latest my-project --typescript. Or add TypeScript to an existing project by installingtypescriptand creating atsconfig.json.How do you generate TypeScript types from content types? Answer: Run
npm run strapi generate:types. This creates type definitions intypes/generated/based on your content type schemas.What is the benefit of TypeScript strict mode? Answer: Strict mode enables
strictNullChecks,noImplicitAny,noImplicitThis, andalwaysStrict. These catch null reference errors, require explicit types, and enforce type-safethisusage.Challenge: Convert an existing JavaScript Strapi project to TypeScript: (1) Set up TypeScript with a
tsconfig.json, (2) Rename.jsfiles to.tsand fix type errors, (3) Generate types for all content types, (4) Rewrite a controller with proper types, (5) Rewrite a service with proper return types, (6) Write a custom type definition for a plugin, (7) Enable strict mode and fix any new errors.
FAQ
Mini Project
Your task: Build a TypeScript Strapi project from scratch.
- Create a new Strapi project with TypeScript.
- Create an "Article" content type with fields: title (string), content (richtext), slug (UID from title), views (integer).
- Run type generation and examine the generated types.
- Write a TypeScript controller that:
- Lists articles with proper typing
- Creates articles with input validation types
- Returns a typed response
- Write a TypeScript service that:
- Increments the view count with typed parameters
- Finds popular articles with typed return
- Write a typed lifecycle hook that:
- Generates a slug if not provided
- Logs creation with typed event data
- Compile and verify no TypeScript errors.
What's Next
Now that you understand TypeScript with Strapi, proceed to Testing to learn about Unit Testing, Integration Testing, and API testing for Strapi applications. After that, prepare for production with Production Setup.
Related lessons:
- Node.js TypeScript — Understanding TypeScript with Node.js
- API Customization — Type-safe API development
- REST API — Typed API responses
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro