Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Example 1: Simple Blog

This is the first worked example: a small blog API built end to end. It uses only what the previous pages covered — models and mfx: tags, the four-step setup, and SQLite. No relations, no middleware, no pipeline customisation yet; those arrive in later chapters. The goal is to see a complete, runnable app with nothing unexplained in it.

What we’re building

A blog with two independent resources:

  • Post — an article with a title, body, and a publication status.
  • Subscriber — an email address signed up for the newsletter.

The two are unrelated — each is its own table with its own endpoints — which keeps the example to concepts already introduced.

The whole app

The blog is small enough to live in a single main.go, the smallest-app shape from App Anatomy:

package main

import (
    "log"

    "github.com/xaleel/maniflex"
    "github.com/xaleel/maniflex/db/sqlite"
)

// Post is a blog article.
type Post struct {
    maniflex.BaseModel
    Title  string `json:"title"  mfx:"required,filterable,sortable"`
    Body   string `json:"body"   mfx:"required"`
    Status string `json:"status" mfx:"required,filterable,sortable,enum:draft|published|archived"`
}

// Subscriber is a newsletter sign-up.
type Subscriber struct {
    maniflex.BaseModel
    Email string `json:"email" mfx:"required,filterable"`
    Name  string `json:"name"  mfx:"filterable,sortable"`
}

func main() {
    // 1. Create the server.
    server := maniflex.New(maniflex.Config{
        Port:        8080,
        PathPrefix:  "/api",
    })

    // 2. Register both models — populates the registry.
    server.MustRegister(Post{}, Subscriber{})

    // 3. Open SQLite with the populated registry, then inject it.
    db, err := sqlite.Open("./blog.db", server.Registry())
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    server.SetDB(db)

    // 4. Serve.
    log.Fatal(server.Start())
}

That is the entire blog. Run it:

go run .

Reading the models

Every field choice maps to a tag covered in Getting Started:

FieldTagsEffect
Titlerequired,filterable,sortablemust be present; usable in ?filter= and ?sort=
Bodyrequiredmust be present; not queryable
Statusrequired,...,enum:draft|published|archivedrejected unless one of the three values
Emailrequired,filterablemust be present; filterable but not sortable
Namefilterable,sortableoptional; queryable and sortable

maniflex.BaseModel adds id, created_at, and updated_at to both structs, so those are never declared by hand. With migration on by default, the posts and subscribers tables are created to match on startup.

The endpoints you get

Registering the two structs mounts a full REST surface under /api:

MethodPath
POST/api/postscreate a post
GET/api/postslist posts
GET/api/posts/{id}read one post
PATCH/api/posts/{id}update a post
DELETE/api/posts/{id}delete a post

Subscriber gets the identical five routes under /api/subscribers.

Using the API

Create a post

curl -X POST localhost:8080/api/posts \
  -H 'Content-Type: application/json' \
  -d '{"title":"Hello World","body":"My first post","status":"draft"}'

The response echoes the stored row, including the id and timestamps that BaseModel filled in.

Validation in action

Leave out a required field, or send a status outside the enum, and the write is rejected before it reaches the database:

# Missing body, and status is not in the enum
curl -X POST localhost:8080/api/posts \
  -H 'Content-Type: application/json' \
  -d '{"title":"Broken","status":"weekly"}'
# → 400, the response names the offending fields

Update and delete

# Publish the post — PATCH only sends the fields that change
curl -X PATCH localhost:8080/api/posts/<id> \
  -H 'Content-Type: application/json' \
  -d '{"status":"published"}'

curl -X DELETE localhost:8080/api/posts/<id>

List, filter, sort, paginate

All from the query string, on the fields tagged filterable / sortable:

# Only published posts
curl 'localhost:8080/api/posts?filter=status:eq:published'

# Newest first
curl 'localhost:8080/api/posts?sort=created_at:desc'

# Page two, five per page
curl 'localhost:8080/api/posts?page=2&limit=5'

# Combine them
curl 'localhost:8080/api/posts?filter=status:eq:published&sort=title:asc&limit=5'

Add a couple of subscribers and the same querying works there too:

curl -X POST localhost:8080/api/subscribers \
  -H 'Content-Type: application/json' \
  -d '{"email":"[email protected]","name":"Ada"}'

curl 'localhost:8080/api/subscribers?sort=name:asc'

The API documents itself

You never wrote a schema, yet the server already publishes one. Alongside the model routes, maniflex auto-generates an OpenAPI 3.1 specification describing every endpoint, field, and validation rule — derived from the same structs and mfx: tags:

curl localhost:8080/api/openapi.json

The spec updates itself whenever a model changes; there is nothing to regenerate. To browse it as interactive documentation, the framework ships an HTML viewer in static/openapi.html that loads /api/openapi.json — open http://localhost:8080/static/openapi.html while the server is running.

The OpenAPI step is fully customisable later; see OpenAPI Spec.

What this example showed

  • A complete, runnable app from two plain structs and a four-step main.
  • mfx: tags driving validation (required, enum) and queryability (filterable, sortable).
  • Multiple models registered in one call, each with an independent REST surface.
  • Filtering, sorting, and pagination with no query code written.
  • A self-updating OpenAPI 3.1 spec at /api/openapi.json.

Where to go next

Everything here treated the two models as separate islands. Real apps connect them — a post belongs to an author, a comment belongs to a post. That, and the mfx: tags beyond the basics, are the next chapters: