Skip to content

Strapi with TypeScript — TypeScript Setup, Type Generation, and Strict Mode

DodaTech Updated 2026-06-28 8 min read

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 types
  • noImplicitAny — must declare types explicitly
  • noImplicitThis — must type this in functions
  • alwaysStrict — 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

  1. Using any everywhere. TypeScript's real benefit is catching type errors. Using any defeats this. Define proper types for your data structures.

  2. Not regenerating types after schema changes. When you add fields or change content types, the generated types become outdated. Run npm run strapi generate:types after every schema change.

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

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

  5. Not typing API responses and requests. The ctx.request.body and API responses benefit significantly from proper typing. Define interfaces for your request and response shapes.

Practice Questions

  1. 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 installing typescript and creating a tsconfig.json.

  2. How do you generate TypeScript types from content types? Answer: Run npm run strapi generate:types. This creates type definitions in types/generated/ based on your content type schemas.

  3. What is the benefit of TypeScript strict mode? Answer: Strict mode enables strictNullChecks, noImplicitAny, noImplicitThis, and alwaysStrict. These catch null reference errors, require explicit types, and enforce type-safe this usage.

  4. Challenge: Convert an existing JavaScript Strapi project to TypeScript: (1) Set up TypeScript with a tsconfig.json, (2) Rename .js files to .ts and 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

Does Strapi compile TypeScript automatically?

Yes, Strapi has built-in TypeScript support. It uses ts-node in development and compiles TypeScript for production. You do not need a separate build step for TypeScript.

Can I have both .js and .ts files in the same project?

Yes, Strapi supports mixed JavaScript and TypeScript projects. TypeScript files are compiled automatically. JavaScript files are used as-is.

How do I type Strapi's entity service methods?

The entity service methods return any by default. Use generated types to cast the results: const articles = await strapi.entityService.findMany(...) as Article[].

Do Strapi plugins support TypeScript?

Some plugins provide TypeScript definitions. Check the plugin's documentation or type definitions. For plugins without types, create custom .d.ts files.

How do I debug TypeScript errors in Strapi?

Run npx tsc --noEmit to check for compilation errors without generating output. Use inline source maps for debugging TypeScript at runtime.

Mini Project

Your task: Build a TypeScript Strapi project from scratch.

  1. Create a new Strapi project with TypeScript.
  2. Create an "Article" content type with fields: title (string), content (richtext), slug (UID from title), views (integer).
  3. Run type generation and examine the generated types.
  4. Write a TypeScript controller that:
    • Lists articles with proper typing
    • Creates articles with input validation types
    • Returns a typed response
  5. Write a TypeScript service that:
    • Increments the view count with typed parameters
    • Finds popular articles with typed return
  6. Write a typed lifecycle hook that:
    • Generates a slug if not provided
    • Logs creation with typed event data
  7. 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:

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro