RaisFastRaisFast
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,instructor

Filter 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

FeatureSource
3 database tablesAuto-migration from TOML
15+ REST endpointsAuto-generated routes
Slug auto-generationuid field type
Timestamps on all recordstimestampable protocol
Author trackingownable protocol
Default orderingsortable protocol
Tree structure for lessonsnestable protocol
Published-only filter for publicstatusable + Rule Engine
Admin-only writesAccess control
Cross-table populateinclude parameter
Junction table for tagsmany_to_many auto-create
Unique slug constraintIndex definition

Zero lines of Rust, zero SQL queries written manually.

On this page