Website Development with Strapi CMS
Strapi is a Node.js headless CMS with automatic REST and GraphQL API generation from content-type configuration. Content types are described via JSON schema (in src/api/*/content-types/*.json), and schema changes made through the GUI are saved to code. It suits projects with partially technical teams: developers define the structure, editors populate content.
Architecture
Browser / Mobile App / Next.js
↕ REST API / GraphQL
Strapi (Node.js)
↕
PostgreSQL / MySQL / SQLite
↕
Cloudinary / S3 (media)
Strapi runs as a separate process. There is no monolithic integration with Next.js — only HTTP requests.
Installation
npx create-strapi-app@latest my-project --quickstart
# Quickstart: SQLite, no customization
# Or with PostgreSQL
npx create-strapi-app@latest my-project \
--dbclient=postgres \
--dbhost=localhost \
--dbport=5432 \
--dbname=strapi_db \
--dbusername=strapi \
--dbpassword=pass
cd my-project
npm run develop # dev mode with hot reload
# Admin: http://localhost:1337/admin
Content Types
Content types are created via Content-Type Builder in admin or manually via JSON:
// src/api/article/content-types/article/schema.json
{
"kind": "collectionType",
"collectionName": "articles",
"info": {
"singularName": "article",
"pluralName": "articles",
"displayName": "Article"
},
"attributes": {
"title": { "type": "string", "required": true },
"slug": { "type": "uid", "targetField": "title" },
"content": { "type": "richtext" },
"excerpt": { "type": "text", "maxLength": 500 },
"publishedAt": { "type": "datetime" },
"cover": { "type": "media", "multiple": false, "required": false, "allowedTypes": ["images"] },
"category": { "type": "relation", "relation": "manyToOne", "target": "api::category.category" },
"tags": { "type": "relation", "relation": "manyToMany", "target": "api::tag.tag" },
"author": { "type": "relation", "relation": "manyToOne", "target": "plugin::users-permissions.user" }
}
}
Typical Project Stack
| Layer | Technology |
|---|---|
| CMS | Strapi 5.x |
| Frontend | Next.js 14 / Nuxt 3 |
| Database | PostgreSQL |
| Media | Cloudinary / AWS S3 |
| CMS Deploy | Railway / Render / VPS |
| Frontend Deploy | Vercel / Netlify |
| Cache | Redis (for production) |
REST API Out of the Box
# Get list of articles (public if permissions configured)
GET http://localhost:1337/api/articles?populate=cover,category,author
# Filtering
GET /api/articles?filters[category][slug][$eq]=tech&sort=publishedAt:desc&pagination[pageSize]=10
# Specific entry
GET /api/articles/123?populate=deep
# Search
GET /api/articles?filters[title][$containsi]=javascript
Integration with Next.js
// lib/strapi.ts
const STRAPI_URL = process.env.STRAPI_URL || 'http://localhost:1337'
const API_TOKEN = process.env.STRAPI_API_TOKEN
export async function fetchStrapi<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const response = await fetch(`${STRAPI_URL}/api${endpoint}`, {
headers: {
Authorization: `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json',
},
next: { tags: [endpoint.split('/')[1]] }, // ISR tag
...options,
})
if (!response.ok) {
throw new Error(`Strapi API error: ${response.status}`)
}
const data = await response.json()
return data
}
// Usage
const { data: articles } = await fetchStrapi<{ data: Article[] }>(
'/articles?populate=cover,category&sort=publishedAt:desc&pagination[pageSize]=10'
)
Webhooks for ISR
// app/api/revalidate/strapi/route.ts
import { revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
export async function POST(req: NextRequest) {
const body = await req.json()
// Strapi sends: { event, uid, model, entry }
const { model } = body
// Invalidate cache by model
revalidateTag(model)
return NextResponse.json({ revalidated: true })
}
In Strapi admin: Settings → Webhooks → Add new webhook → URL of your Next.js endpoint.
Development Features
Strapi Response Format: all data is wrapped in { data: { id, attributes: {...} } }. In Strapi 5, this changed — data is returned flat.
Populate: by default, relationships are not filled. You must explicitly specify ?populate=* or ?populate[category][populate][0]=icon.
Drafts: Draft/Publish system is built-in. ?publicationState=live — published only, ?publicationState=preview + API token — drafts.
Timeline
A basic website with 4–6 content types, permissions setup, and Next.js integration takes 2–3 weeks. Complex projects with custom controllers, plugins, and multilingual support take 4–6 weeks.







