Content Types
Real Example — Online Course Platform
Build a complete multi-table online course platform with instructors, courses, and lessons using Content Types.
This walkthrough builds a real-world multi-table system: an online course platform with three linked content types — Instructor, Course, and Lesson.
Data Model
Instructor ──< Course >── Tag
│
└──< Lesson- An Instructor has many Courses (
one_to_many) - A Course belongs to an Instructor (
many_to_one) - A Course has many Lessons (
one_to_many) - A Course has many Tags via junction table (
many_to_many) - A Lesson belongs to a Course (
many_to_one)
1. Instructor
Create extensions/content_types/instructor.toml:
[content_type]
name = "Instructor"
singular = "instructor"
plural = "instructors"
table = "instructors"
[[fields]]
name = "name"
field_type = "text"
required = true
[[fields]]
name = "bio"
field_type = "richtext"
[[fields]]
name = "avatar"
field_type = "media"
media_config = { accept = ["image/*"], max_count = 1 }
[[fields]]
name = "email"
field_type = "email"
required = true
unique = true
[[fields]]
name = "website"
field_type = "text"
[[fields]]
name = "courses"
field_type = "relation"
relation = { relation_type = "one_to_many", target = "courses" }
[content_type.implements]
protocols = ["timestampable"]2. Course
Create extensions/content_types/course.toml:
[content_type]
name = "Course"
singular = "course"
plural = "courses"
table = "courses"
[[fields]]
name = "title"
field_type = "text"
required = true
max_length = 200
[[fields]]
name = "slug"
field_type = "uid"
target_field = "title"
[[fields]]
name = "cover"
field_type = "media"
media_config = { accept = ["image/*"], max_count = 1 }
[[fields]]
name = "description"
field_type = "text"
max_length = 500
[[fields]]
name = "price"
field_type = "decimal"
min = 0
[[fields]]
name = "level"
field_type = "enum"
enum_values = ["beginner", "intermediate", "advanced"]
default = "beginner"
[[fields]]
name = "instructor"
field_type = "relation"
relation = { relation_type = "many_to_one", target = "instructors" }
[[fields]]
name = "lessons"
field_type = "relation"
relation = { relation_type = "one_to_many", target = "lessons" }
[[fields]]
name = "tags"
field_type = "relation"
relation = { relation_type = "many_to_many", target = "tags", through = "courses_tags" }
[[fields]]
name = "status"
field_type = "enum"
enum_values = ["draft", "published", "archived"]
default = "draft"
[[indexes]]
fields = ["slug"]
unique = true
[content_type.implements]
protocols = ["timestampable", "ownable", "sortable"]
[[content_type.implements.protocols]]
name = "statusable"
values = "draft,published,archived"
default = "draft"
[content_type.api.list]
access = "public"
filter = 'status = "published"'
fields = ["title", "slug", "cover", "price", "level", "instructor"]
[content_type.api.create]
access = "admin"
[content_type.api.update]
access = "admin"
[content_type.api.delete]
access = "admin"3. Lesson
Create extensions/content_types/lesson.toml:
[content_type]
name = "Lesson"
singular = "lesson"
plural = "lessons"
table = "lessons"
[[fields]]
name = "title"
field_type = "text"
required = true
[[fields]]
name = "content"
field_type = "richtext"
[[fields]]
name = "video_url"
field_type = "text"
[[fields]]
name = "duration_minutes"
field_type = "integer"
min = 1
[[fields]]
name = "is_free"
field_type = "boolean"
default = false
[[fields]]
name = "course"
field_type = "relation"
relation = { relation_type = "many_to_one", target = "courses" }
[content_type.implements]
protocols = ["timestampable", "sortable", "nestable"]4. Tag (optional)
Create extensions/content_types/tag.toml:
[content_type]
name = "Tag"
singular = "tag"
plural = "tags"
table = "course_tags"
[[fields]]
name = "name"
field_type = "text"
required = true
unique = true
[content_type.implements]
protocols = ["timestampable"]Result
Start the server. Three tables are auto-created with full CRUD APIs and cross-table relation resolution.
List courses with instructor and tags
curl http://localhost:9898/api/v1/cms/courses?include=instructor,tags{
"items": [
{
"id": "abc123",
"title": "Rust for Beginners",
"slug": "rust-for-beginners",
"price": "29.99",
"level": "beginner",
"status": "published",
"instructor": {
"id": "inst1",
"name": "Alice Chen",
"email": "alice@example.com"
},
"tags": [
{ "id": "t1", "name": "Rust" },
{ "id": "t2", "name": "Systems Programming" }
]
}
],
"total": 1,
"page": 1,
"page_size": 20
}Get a course with lessons and instructor
curl http://localhost:9898/api/v1/cms/courses/abc123?include=lessons,instructorFilter lessons by course
curl "http://localhost:9898/api/v1/cms/lessons?course_id=abc123&sort=sort_key:asc"Create a course (admin only)
curl -X POST http://localhost:9898/api/v1/cms/courses \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Rust for Beginners",
"price": 29.99,
"level": "beginner",
"instructor": "inst1",
"tags": ["t1", "t2"],
"status": "published"
}'Create a lesson
curl -X POST http://localhost:9898/api/v1/cms/lessons \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Hello World",
"content": "<p>Your first Rust program</p>",
"duration_minutes": 15,
"is_free": true,
"course": "abc123"
}'Update a lesson
curl -X PUT http://localhost:9898/api/v1/cms/lessons/{id} \
-H "Authorization: Bearer TOKEN" \
-H "Content-Type: application/json" \
-d '{"duration_minutes": 20}'Search courses
curl "http://localhost:9898/api/v1/cms/courses?search=rust&include=instructor"What You Got From This
| Feature | Source |
|---|---|
| 3 database tables | Auto-migration from TOML |
| 15+ REST endpoints | Auto-generated routes |
| Slug auto-generation | uid field type |
| Timestamps on all records | timestampable protocol |
| Author tracking | ownable protocol |
| Default ordering | sortable protocol |
| Tree structure for lessons | nestable protocol |
| Published-only filter for public | statusable + Rule Engine |
| Admin-only writes | Access control |
| Cross-table populate | include parameter |
| Junction table for tags | many_to_many auto-create |
| Unique slug constraint | Index definition |
Zero lines of Rust, zero SQL queries written manually.
