Skip to content
Learni
View all tutorials
Développement Backend

How to Deploy a Headless CMS with Strapi in 2026

Lire en français

Introduction

A Headless CMS separates content from presentation, giving developers complete flexibility. Strapi 5, with native TypeScript support, makes it easy to build robust and secure APIs. This tutorial walks you through an advanced setup that includes complex content types, custom middlewares, and an optimized deployment process. You will learn how to avoid common pitfalls while following production-grade standards.

Prerequisites

  • Node.js 20+ and npm/yarn
  • Solid knowledge of TypeScript and REST/GraphQL
  • An account with a hosting provider (Vercel, Railway or VPS)
  • Strapi 5 CLI installed globally

Initialize the Strapi Project

terminal
npx create-strapi-app@latest my-cms --quickstart --ts
cd my-cms
npm run develop

This command creates a Strapi 5 project with TypeScript enabled by default. The --quickstart flag starts a SQLite database for rapid development.

Create the Article Content Type

src/api/article/content-types/article/schema.json
{
  "kind": "collectionType",
  "collectionName": "articles",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article"
  },
  "options": {
    "draftAndPublish": true
  },
  "attributes": {
    "title": {
      "type": "string",
      "required": true
    },
    "content": {
      "type": "richtext"
    },
    "slug": {
      "type": "uid",
      "targetField": "title"
    }
  }
}

The schema.json file defines the Article model. The uid field automatically generates unique slugs from the title.

Configure API Permissions

config/api.ts
export default {
  rest: {
    defaultLimit: 25,
    maxLimit: 100,
    withCount: true,
  },
  response: {
    status: 200,
    headers: {
      'Cache-Control': 'public, max-age=300',
    },
  },
};

This file adjusts default limits and adds cache headers. It improves performance for public requests while remaining secure.

Add a Custom Controller

src/api/article/controllers/article.ts
import { factories } from '@strapi/strapi';

export default factories.createCoreController('api::article.article', ({ strapi }) => ({
  async findBySlug(ctx) {
    const { slug } = ctx.params;
    const article = await strapi.db.query('api::article.article').findOne({
      where: { slug },
      populate: ['author'],
    });
    if (!article) return ctx.notFound();
    return article;
  },
}));

This controller adds a custom route /articles/slug/:slug. It uses the query builder for optimal performance with relation population.

Next.js Integration with Typed Fetch

app/articles/[slug]/page.tsx
export async function getArticle(slug: string) {
  const res = await fetch(`${process.env.STRAPI_URL}/api/articles/slug/${slug}`, {
    headers: { Authorization: `Bearer ${process.env.STRAPI_TOKEN}` },
    next: { revalidate: 300 },
  });
  return res.json();
}

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const data = await getArticle(params.slug);
  return <h1>{data.title}</h1>;
}

This Next.js code fetches data from Strapi with ISR revalidation. Use environment variables for tokens to keep access secure.

Best Practices

  • Always enable draftAndPublish for critical content
  • Use middlewares for validation and sanitization
  • Configure strict request limits in production
  • Version your content types with migrations
  • Enable structured logging for monitoring

Common Mistakes to Avoid

  • Forgetting to regenerate TypeScript types after schema changes
  • Leaving public permissions too permissive
  • Ignoring CORS configuration for frontend origins
  • Not setting up automatic database backups

Further Reading

Deepen your skills with our advanced Headless CMS courses.

How to Deploy Strapi Headless CMS with TypeScript in 2026 | Learni