Getting Started
This walks from an empty module to a running API with live docs. It assumes Go 1.25 or newer and a database GORM can reach — the examples use Postgres.
Install the CLI
go install github.com/caspel26/goninja/cmd/goninja@latestAdd the runtime to your module:
go get github.com/caspel26/goninjaDefine a model
A model is a normal GORM struct. The goninja tag on each field decides which
operations expose it; a field with no tag never appears in any generated type.
package models
import "time"
type Author struct {
ID string `gorm:"primaryKey;type:uuid" goninja:"list,retrieve"`
Name string `gorm:"size:120;not null" goninja:"list,retrieve,create,update" validate:"required,max=120"`
Country string `gorm:"size:2" goninja:"list,retrieve,create,update,filter" validate:"omitempty,len=2"`
CreatedAt time.Time `goninja:"list,retrieve"`
}package models
import "time"
type Book struct {
ID string `gorm:"primaryKey;type:uuid" goninja:"list,retrieve"`
Title string `gorm:"size:200;not null" goninja:"list,retrieve,create,update" validate:"required,max=200"`
AuthorID string `goninja:"list,retrieve,create,update,filter" validate:"required,uuid4"`
Price float64 `goninja:"list,retrieve,create,update,filter" validate:"min=0"`
Published bool `goninja:"list,retrieve,create,update,filter"`
CreatedAt time.Time `goninja:"list,retrieve"`
Author Author `goninja:"retrieve"`
}Author on Book is a GORM belongs-to relation, inferred from the AuthorID
field by GORM’s naming convention. Tagging it retrieve means the detail view
nests the full author and preloads it; the list view never does. See
Struct Tags for the full vocabulary.
An ID field is required and must be named literally ID. A string ID is
treated as a UUID primary key and filled in on create; an int64 ID is parsed
from the path with strconv.ParseInt.
Generate
goninja generate -models-import myapp/models-models-import is the only required flag — it is the import path of your
models package, written into the generated import block. Everything else has a
default: models are read from ./models, code is written to ./internal/api,
and the generated package is named api.
That writes one file per model:
- author_generated.go
- book_generated.go
Each file carries a DO NOT EDIT header. Commit it — it is part of your source
tree, and reviewing its diff is how you see the effect of a tag change.
goninja generate -watch -models-import myapp/models and
it regenerates on every save, debounced so one save is one regeneration.Mount it
package main
import (
"log"
"net/http"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"github.com/caspel26/goninja"
"github.com/caspel26/goninja/docsui"
"myapp/internal/api"
"myapp/models"
)
func main() {
db, err := gorm.Open(postgres.Open(os.Getenv("DSN")), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// goninja does not generate migrations.
if err := db.AutoMigrate(&models.Author{}, &models.Book{}); err != nil {
log.Fatal(err)
}
mux := http.NewServeMux()
app := goninja.NewAPI("Bookstore API", "0.1.0")
app.Mount(mux,
api.NewAuthorResource(db),
api.NewBookResource(db),
)
app.MountDocs(mux, "/docs", docsui.SwaggerUI())
log.Fatal(http.ListenAndServe(":8080", mux))
}Mount registers each resource’s routes on the mux and merges its OpenAPI
fragment into one document, which MountDocs then serves alongside a rendered
UI.
What you get
| Method | Path | Description |
|---|---|---|
GET | /books | list, with filters, ordering and pagination |
POST | /books | create, validated against the validate tags |
GET | /books/{id} | retrieve, with relations preloaded |
PUT | /books/{id} | update |
DELETE | /books/{id} | delete |
GET | /docs/ | Swagger UI over the merged OpenAPI document |
Same for /authors. Run it and try a query:
curl "localhost:8080/books?published=true&price_min=10&order=-created_at&limit=20"{
"items": [
{
"id": "0f3d9a3e-6c1b-4a2f-9f77-2b1c8e5d4a10",
"title": "The Go Programming Language",
"author_id": "b21e5c74-9f0a-4c33-8f21-6de0a1b7c559",
"price": 34.99,
"published": true,
"created_at": "2026-08-20T09:14:02Z"
}
],
"total": 128,
"limit": 20,
"offset": 0
}List responses are wrapped in that envelope; retrieve, create and update return the object directly. A validation failure returns 422 with a per-field body:
{
"code": "VALIDATION_FAILED",
"errors": { "title": "required" }
}