Strapi 5: Open Source Headless CMS Guide
Strapi 5 has arrived with a fresh architecture, a more modular core, and a revamped plugin ecosystem. If you’re looking for a flexible, open‑source headless CMS that can power everything from a simple blog to a complex e‑commerce platform, Strapi is a solid choice. In this guide we’ll walk through setting up a Strapi 5 project, creating content types, securing your API, and finally consuming the data with a Python client. By the end you’ll have a production‑ready backend you can extend however you like.
Getting Started with Strapi 5
Before diving into code, make sure your development environment meets the minimum requirements. Strapi 5 runs on Node.js ≥ 18, and you’ll need npm ≥ 9 or Yarn ≥ 3. A recent version of PostgreSQL, MySQL, or SQLite works for the database layer; SQLite is perfect for quick prototypes.
Open a terminal and run the following command to create a new project. The --quickstart flag spins up a SQLite database and launches the admin panel automatically.
npx create-strapi-app my-cms --quickstart
After a few minutes you’ll see a URL like http://localhost:1337/admin. Register the first admin user, and you’re ready to explore the dashboard. The UI is built with React, but you’ll spend most of your time in the codebase, which lives under the src folder.
Project Structure Overview
Strapi 5 introduces a more intuitive folder layout. Here’s a quick rundown of the most important directories:
- src/api – Holds your content‑type definitions and business logic.
- src/plugins – Where community or custom plugins live.
- src/config – Global configuration files (database, server, middleware).
- src/admin – Customizations for the admin panel UI.
Each content type gets its own subfolder inside src/api, containing content-types, controllers, services, and routes. This separation encourages clean, maintainable code and makes it easy to add new features later.
Creating Your First Content Type
Let’s build a simple Article model with fields for title, body, and a featured image. Strapi’s CLI makes this painless:
cd my-cms
npm run strapi generate:api article
When prompted, select collection type and add the following fields:
- Title –
String, required. - Body –
Rich Text, required. - Featured Image –
Media, single.
The CLI updates src/api/article/content-types/article/schema.json and creates boilerplate controller, service, and route files. You can now manage articles directly from the admin UI.
Working with the API
Strapi automatically generates REST and GraphQL endpoints for every content type. By default, the REST endpoint for our article collection is GET /api/articles. Let’s fetch the data using Python’s requests library.
import requests
BASE_URL = "http://localhost:1337/api/articles"
def fetch_articles():
response = requests.get(BASE_URL)
response.raise_for_status()
data = response.json()
for article in data["data"]:
attrs = article["attributes"]
print(f"Title: {attrs['title']}")
print(f"Excerpt: {attrs['body'][:100]}...")
print("-" * 40)
if __name__ == "__main__":
fetch_articles()
Running this script prints a list of article titles and a short excerpt. The JSON payload follows the JSON:API spec, with data and attributes keys. You can also filter, sort, and paginate directly via query parameters – for example, /api/articles?sort=createdAt:desc&pagination[page]=2&pagination[pageSize]=5.
Pro tip: Enablepopulatefor media fields when you need the image URL:/api/articles?populate=featuredImage.
Securing the API with JWT
In production you’ll want to protect your endpoints. Strapi ships with a JWT‑based authentication system. First, register a user via the /api/auth/local/register endpoint, then log in to receive a token.
def register_user(email, password):
payload = {"email": email, "password": password, "username": email}
r = requests.post("http://localhost:1337/api/auth/local/register", json=payload)
r.raise_for_status()
return r.json()["jwt"]
def login_user(email, password):
payload = {"identifier": email, "password": password}
r = requests.post("http://localhost:1337/api/auth/local", json=payload)
r.raise_for_status()
return r.json()["jwt"]
Once you have the token, include it in the Authorization header for subsequent requests:
def get_protected_articles(jwt):
headers = {"Authorization": f"Bearer {jwt}"}
r = requests.get("http://localhost:1337/api/articles", headers=headers)
r.raise_for_status()
return r.json()
Remember to adjust the Roles & Permissions panel in the admin UI. By default, the Public role can read public content, but you can revoke that and grant access only to authenticated users.
Customizing Permissions and Roles
Strapi’s permission system is granular down to individual actions (find, findOne, create, update, delete). Navigate to Settings → Roles & Permissions → Public to toggle which endpoints are exposed without a token. For a private API, disable find and findOne for the Public role, then enable them for the Authenticated role.
Beyond built‑in roles, you can create custom roles for editors, moderators, or API clients. Each role can have its own set of allowed actions and field‑level restrictions, which is especially useful for multi‑tenant SaaS applications.
Adding Business Logic with Controllers
Sometimes you need to enrich the response or enforce additional validation. Strapi’s controller layer lets you intercept requests. Open src/api/article/controllers/article.js and add a custom method:
module.exports = {
async latest(ctx) {
const articles = await strapi.service('api::article.article').find({
sort: { createdAt: 'desc' },
pagination: { limit: 5 },
});
ctx.send(articles);
},
};
Register the route in src/api/article/routes/article.js:
module.exports = {
routes: [
{
method: 'GET',
path: '/articles/latest',
handler: 'article.latest',
config: {
policies: [],
middlewares: [],
},
},
],
};
Now GET /api/articles/latest returns the five most recent articles, perfect for a homepage carousel.
Pro tip: Use Strapi’s eventHub to listen for lifecycle events (beforeCreate, afterUpdate) and trigger side effects such as sending emails or updating a search index.
Extending Strapi with Plugins
One of Strapi’s biggest strengths is its plugin architecture. The official marketplace offers plugins for SEO, GraphQL, S3 uploads, and more. Installing a plugin is as easy as running an npm command and adding it to src/config/plugins.js.
npm install @strapi/plugin-graphql
Then enable it:
module.exports = ({ env }) => ({
// other plugins …
graphql: {
enabled: true,
config: {
endpoint: '/graphql',
shadowCRUD: true,
playgroundAlways: true,
},
},
});
With GraphQL enabled, you can query articles like this:
query {
articles {
data {
id
attributes {
title
body
featuredImage {
data {
attributes {
url
}
}
}
}
}
}
}
For custom business needs, you might build a plugin that integrates with a third‑party translation service. Strapi provides a register() hook where you can inject services, routes, or even admin UI components.
Example: Simple SEO Plugin
Below is a minimal plugin that adds an seoMeta field to every content type and automatically generates meta tags on the server side.
// src/plugins/seo/index.js
module.exports = (plugin) => {
plugin.routes['content-api'].push({
method: 'GET',
path: '/:contentType/:id/seo',
handler: 'seo.generate',
});
return plugin;
};
The corresponding service could look like this:
// src/plugins/seo/services/seo.js
module.exports = {
async generate(ctx) {
const { contentType, id } = ctx.params;
const entry = await strapi.entityService.findOne(contentType, id, { populate: '*' });
const meta = {
title: entry.title,
description: entry.body?.slice(0, 160),
image: entry.featuredImage?.url,
};
ctx.send({ meta });
},
};
After installing, you can call /api/articles/42/seo to retrieve ready‑to‑use meta tags for SEO purposes.
Deploying Strapi 5 to Production
Strapi runs anywhere Node.js runs: VPS, Docker, serverless, or Platform‑as‑a‑Service providers like Render or Railway. For most teams, Docker offers the most reproducible environment.
Here’s a minimal Dockerfile for a Strapi 5 app using PostgreSQL as the database:
FROM node:18-alpine
# Create app directory
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --production
# Copy source code
COPY . .
# Build admin UI
RUN npm run build
EXPOSE 1337
CMD ["npm", "run", "start"]
In .env set the database connection string:
DATABASE_CLIENT=postgres
DATABASE_URL=postgres://user:password@db:5432/strapi
HOST=0.0.0.0
PORT=1337
Compose everything together:
version: "3.8"
services:
db:
image: postgres:15
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: strapi
volumes:
- db_data:/var/lib/postgresql/data
cms:
build: .
env_file: .env
ports:
- "1337:1337"
depends_on:
- db
volumes:
db_data:
Run docker compose up -d and Strapi will start connected to the PostgreSQL container. Remember to set APP_KEYS, API_TOKEN_SALT, and JWT_SECRET to strong random values in production.
Performance Considerations
- Cache responses with a CDN or reverse proxy (e.g., Cloudflare, Nginx) for public endpoints.
- Enable compression in
src/middlewares/compression.jsto reduce payload size. - Use pagination on list endpoints to avoid massive JSON responses.
- Leverage database indexes on frequently filtered fields (e.g.,
slug,publishedAt).
Real‑World Use Cases
1. Multi‑Language Blog – Combine Strapi’s i18n plugin with a static site generator like Next.js. Store each article in multiple locales, and let the front‑end fetch the appropriate language based on the URL.
2. E‑Commerce Catalog – Model Product, Category, and Variant collections. Use the Media library for product images and the Upload plugin for bulk import. Pair Strapi with a headless checkout service (e.g., Stripe) for a complete storefront.
3. Mobile App Backend – Strapi’s lightweight REST endpoints are perfect for React Native or Flutter apps. Use JWT authentication, role‑based permissions, and webhooks to sync user actions with third‑party services like Firebase.
Pro Tips & Common Pitfalls
Tip 1 – Keep your schema DRY. When several content types share fields (e.g., seoMeta), extract them into a reusable component. Components can be nested, versioned, and reused across the entire project.
Tip 2 – Use webhooks for decoupled workflows. Strapi can fire HTTP POST requests on create, update, or delete events. Hook them up to CI pipelines, search indexers (Algolia, Elastic), or email services.
Tip 3 – Guard against over‑exposing data. The admin UI can edit any field, but the public API only returns what you allow. Double‑check the Public role permissions after adding new fields or relations.
Conclusion
Strapi 5 delivers a modern, modular headless CMS that balances developer freedom with out‑of‑the‑box productivity. By following this guide you’ve set up a Strapi project, defined a content model, secured the API with JWT, extended functionality via custom controllers and plugins, and deployed the whole stack with Docker. Whether you’re building a blog, a product catalog, or a mobile backend, Strapi’s extensible architecture lets you iterate quickly while keeping the codebase clean. Dive deeper into the official documentation, experiment with the plugin ecosystem, and start turning your content into powerful digital experiences.