Complete technical documentation for the Educado Strapi CMS backend.
The backend is built on Strapi v5 (headless CMS) and provides a flexible content management system with auto-generated APIs, authentication, and media management. It serves the web application and provides content creators with tools to manage courses, lectures, exercises, and student data.
- Strapi v5 - Headless CMS framework
- Node.js 22 - Runtime environment
- TypeScript - Type-safe development
- PostgreSQL 16 - Primary database
- Auto-generated REST & GraphQL APIs
- Built-in authentication (JWT + API tokens)
- Role-based access control (RBAC)
- Media library with upload processing
- Extensible plugin system
- Content versioning
strapi/
├── src/
│ ├── index.ts # Entry point
│ ├── api/ # Content types
│ │ ├── certificate/
│ │ ├── content-creator/
│ │ ├── course/
│ │ ├── course-category/
│ │ ├── course-selection/
│ │ ├── exercise/
│ │ ├── exercise-option/
│ │ ├── feedback/
│ │ ├── lecture/
│ │ └── student/
│ ├── components/ # Reusable content components
│ │ └── content/
│ ├── extensions/ # Core modifications
│ │ └── documentation/
│ └── admin/ # Admin panel customization
├── config/ # Configuration files
│ ├── admin.ts
│ ├── api.ts
│ ├── database.ts
│ ├── middlewares.ts
│ ├── plugins.ts
│ └── server.ts
├── database/
│ └── migrations/ # Database migrations
├── public/
│ ├── uploads/ # User-uploaded media
│ └── robots.txt
└── types/
└── generated/ # Auto-generated types
- Fields: title, description, category, lectures, exercises, difficulty level
- Relations:
- Has many lectures (one-to-many)
- Has many exercises (one-to-many)
- Belongs to category (many-to-one)
- Has many course selections (students enrolled)
- Features: Rich text editor, media attachments, versioning
- Fields: title, content, order, duration, resources
- Relations:
- Belongs to course (many-to-one)
- Has media (images, videos, documents)
- Features: Content blocks (text, code, quiz), ordering
- Fields: question, type (multiple choice, free text), difficulty, points
- Relations:
- Belongs to course (many-to-one)
- Has many options (for multiple choice)
- Features: Auto-grading support, feedback system
- Fields: name, email, enrolled courses, progress tracking
- Relations:
- Has many course selections
- Has many feedback submissions
- Features: Progress analytics, certification tracking
- Fields: name, bio, expertise, courses created
- Relations:
- Has many courses (author)
- Features: Profile management, analytics
Reusable content structures:
- Content.TextBlock - Rich text with formatting
- Content.CodeBlock - Syntax-highlighted code snippets
- Content.MediaBlock - Images, videos, embeds
- Content.QuizBlock - Interactive questions
All content types automatically expose CRUD endpoints:
GET /api/courses # List all courses
GET /api/courses/:id # Get single course
POST /api/courses # Create course
PUT /api/courses/:id # Update course
DELETE /api/courses/:id # Delete course
# Login
POST /api/auth/local
{
"identifier": "user@example.com",
"password": "password123"
}
# Returns JWT token for subsequent requests
{
"jwt": "eyJhbGc...",
"user": { ... }
}
# Use token in requests
Authorization: Bearer eyJhbGc...For server-to-server communication:
- Create token in Admin Panel: Settings → API Tokens
- Set permissions (read-only, full access, custom)
- Use in requests:
Authorization: Bearer <api_token>Available at /graphql:
query {
courses {
data {
id
attributes {
title
description
lectures {
data {
attributes {
title
}
}
}
}
}
}
}Configured in config/database.ts:
export default ({ env }) => ({
connection: {
client: 'postgres',
connection: {
host: env('DATABASE_HOST', '127.0.0.1'),
port: env.int('DATABASE_PORT', 5432),
database: env('DATABASE_NAME', 'strapi'),
user: env('DATABASE_USERNAME', 'strapi'),
password: env('DATABASE_PASSWORD', 'strapi'),
ssl: env.bool('DATABASE_SSL', false)
}
}
});Server configuration in config/server.ts:
export default ({ env }) => ({
host: env('HOST', '0.0.0.0'),
port: env.int('PORT', 1337),
url: env('STRAPI_URL', 'http://localhost:1337'),
app: {
keys: env.array('APP_KEYS')
},
webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false)
}
});Required variables (in root .env):
# Server
HOST=0.0.0.0
PORT=1337
STRAPI_URL=http://localhost:1337
# Database
DATABASE_CLIENT=postgres
DATABASE_HOST=postgres
DATABASE_PORT=5432
DATABASE_NAME=strapi
DATABASE_USERNAME=strapi
DATABASE_PASSWORD=strapi_password
DATABASE_SSL=false
# Security
APP_KEYS=key1,key2,key3,key4
API_TOKEN_SALT=random_salt
ADMIN_JWT_SECRET=random_secret
TRANSFER_TOKEN_SALT=random_salt
JWT_SECRET=random_secret
# Admin
ADMIN_PATH=/admin
ADMIN_EMAIL=admin@educado.com
ADMIN_PASSWORD=secure_passwordGenerate secrets:
openssl rand -base64 32- Upload - Media library and file uploads
- Users & Permissions - Authentication and RBAC
- Documentation - OpenAPI spec generation
- GraphQL - GraphQL API
- i18n - Internationalization (if enabled)
In config/plugins.ts:
export default ({ env }) => ({
documentation: {
enabled: true,
config: {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Educado API',
description: 'API documentation for Educado backend'
}
}
},
upload: {
config: {
sizeLimit: 10 * 1024 * 1024 // 10MB
}
}
});# From repository root
npm run dev:strapi
# Or from strapi directory
cd strapi
npm run developAdmin panel: http://localhost:1337/admin
Strapi uses automatic schema synchronization in development. For production:
# Export schema
npm run strapi export
# Import schema
npm run strapi importAdd custom controllers, services, or routes:
// src/api/course/controllers/course.ts
export default {
async customEndpoint(ctx) {
const data = await strapi.service('api::course.course').customMethod();
ctx.body = data;
}
};
// src/api/course/routes/custom-routes.ts
export default {
routes: [
{
method: 'GET',
path: '/courses/custom',
handler: 'course.customEndpoint'
}
]
};Add logic before/after CRUD operations:
// src/api/course/content-types/course/lifecycles.ts
export default {
async beforeCreate(event) {
// Validate or modify data before creation
event.params.data.slug = slugify(event.params.data.title);
},
async afterCreate(event) {
// Trigger actions after creation
await strapi.service('api::notification.notification')
.notifyNewCourse(event.result);
}
};# Using curl
curl http://localhost:1337/api/courses \
-H "Authorization: Bearer <token>"
# Using Postman or Insomnia
# Import OpenAPI spec from /documentation# Run tests (if configured)
npm run testStrapi automatically generates OpenAPI specs via the Documentation plugin:
Endpoints:
- OpenAPI JSON: http://localhost:1337/documentation/json
- OpenAPI YAML: http://localhost:1337/documentation/yaml (if enabled)
Usage:
# Generate web client from spec
cd web
npm run generate-strapi-clientThis keeps the frontend API client in sync with the backend automatically.
// In schema.json for content type
{
"indexes": [
{
"name": "course_title_idx",
"columns": ["title"]
}
]
}Configure in config/middlewares.ts:
export default [
'strapi::errors',
{
name: 'strapi::cache',
config: {
type: 'mem',
maxAge: 3600000 // 1 hour
}
},
// ... other middlewares
];// ❌ Bad: N+1 queries
const courses = await strapi.entityService.findMany('api::course.course');
for (const course of courses) {
const lectures = await strapi.entityService.findMany('api::lecture.lecture', {
filters: { course: course.id }
});
}
// ✅ Good: Single query with population
const courses = await strapi.entityService.findMany('api::course.course', {
populate: ['lectures']
});Production-ready container:
# Build image
npm run docker:build:strapi
# Run container
docker run -p 1337:1337 \
-e DATABASE_HOST=postgres \
-e DATABASE_PASSWORD=secret \
educado-strapi:latestProduction checklist:
- ✅ Set
NODE_ENV=production - ✅ Use strong secrets (APP_KEYS, JWT_SECRET, etc.)
- ✅ Enable SSL for database connection
- ✅ Configure proper CORS origins
- ✅ Set up automated backups
- ✅ Configure logging
# Backup PostgreSQL
pg_dump -h localhost -U strapi strapi > backup.sql
# Restore
psql -h localhost -U strapi strapi < backup.sql- API Tokens: Use read-only tokens when possible
- Rate Limiting: Configure in
config/middlewares.ts - CORS: Whitelist specific origins
- SSL: Always use HTTPS in production
- Secrets: Never commit secrets to git
- Permissions: Follow principle of least privilege
Configure in Admin Panel:
- Settings → Users & Permissions → Roles
- Define roles (Public, Authenticated, Admin, etc.)
- Set permissions per content type and action
- Assign users to roles
Database connection fails
- Verify PostgreSQL is running
- Check DATABASE_* environment variables
- Test connection:
psql -h localhost -U strapi -d strapi
Admin panel won't load
- Clear
.strapicache:rm -rf .strapi - Rebuild admin:
npm run build - Check browser console for errors
API returns 403 Forbidden
- Check API token is valid
- Verify permissions for content type
- Ensure CORS is configured correctly
Migrations fail
- Backup database first
- Check migration files in
database/migrations/ - Manually run SQL if needed
Enable detailed logging:
NODE_ENV=development
STRAPI_LOG_LEVEL=debug# Create new content type
npm run strapi generate
# Build admin panel
npm run build
# Export/import data
npm run strapi export
npm run strapi import
# Database operations
npm run strapi database:migrate
npm run strapi database:reset
# User management
npm run strapi admin:create-user
npm run strapi admin:reset-password// ✅ Good: Clear relationships
{
"course": {
"lectures": { "type": "relation", "relation": "oneToMany" },
"category": { "type": "relation", "relation": "manyToOne" }
}
}
// ❌ Bad: Storing relations as JSON
{
"course": {
"lectureIds": { "type": "json" }
}
}// ✅ Good: Use populate for related data
const course = await strapi.entityService.findOne('api::course.course', id, {
populate: {
lectures: true,
category: true,
author: {
fields: ['name', 'email']
}
}
});
// ❌ Bad: Manual joins
const course = await strapi.entityService.findOne('api::course.course', id);
const lectures = await findLecturesByCourse(course.id);// ✅ Good: Proper error responses
async customAction(ctx) {
try {
const data = await someOperation();
return ctx.send(data);
} catch (error) {
return ctx.badRequest('Operation failed', { error: error.message });
}
}