Skip to content

Getting Started

devituz edited this page Jun 3, 2026 · 1 revision

Getting started

This guide takes you from an empty directory to a running JSON API — with migrations, models, factories, and a Laravel-style HTTP server — in roughly ten minutes. By the end you'll have run every command you'd use on a real project.

If you only want to skim the API surface, the Web-Framework and ORM pages are the dense references. This page is the walkthrough.

1. Install

You need Go 1.25 or newer (the module is on the Go 1.25 toolchain).

mkdir myapp && cd myapp
go mod init github.com/you/myapp

# Library
go get github.com/devituz/lagodev@latest

# CLI — produces a `lago` binary in $(go env GOPATH)/bin
go install github.com/devituz/lagodev/cmd/lago@latest

lago and artisan are interchangeable aliases of the same CLI. Pick either; the rest of this guide uses lago.

Tip: add $(go env GOPATH)/bin to your PATH if it isn't already (export PATH="$(go env GOPATH)/bin:$PATH").

2. Scaffold the project

lago init           # writes lago.json, config/, routes/
lago env:init       # writes .env with documented defaults

You now have:

myapp/
├── .env                     ← DB_CONNECTION, APP_*, etc.
├── lago.json                ← directory layout for generators
├── config/
│   ├── app.go               ← AppConfig from env
│   └── database.go          ← database.Config from env
└── routes/
    └── api.go               ← routes.Register(app)

The default .env points at SQLite:

DB_CONNECTION=sqlite
DB_DATABASE=app.db

Switch to Postgres or MySQL by editing those lines — see Configuration for the full list of variables.

3. Generate your first resource

lago make:model Post -mfsc \
    --fields="title:string,body:text,published:bool:default(false)"

The -mfsc flags compose: migration, factory, seeder, service + controller. All six artifacts share the same field spec, so the model struct, the migration columns, and the factory's faker defaults stay in lockstep.

After the command:

models/post.go                              ← type Post struct { orm.Model; … }
migrations/2026_..._create_posts_table.go   ← schema.Create("posts", …)
factories/post_factory.go                   ← factory.New[Post]
seeders/post_seeder.go                      ← registers itself in init()
services/post_service.go                    ← List/Get/Create/Update/Delete
controllers/post_controller.go              ← *web.Context handlers

Inspect controllers/post_controller.go — every method is one or two lines because the framework auto-converts the (any, error) return into the right HTTP response (see Web-Framework for the full contract).

4. Wire the route

Edit routes/api.go:

package routes

import (
    "github.com/devituz/lagodev/web"
    "github.com/you/myapp/controllers"
)

func Register(app *web.App) {
    app.Get("/health", func(c *web.Context) (any, error) {
        return map[string]string{"status": "ok"}, nil
    })

    app.Group("/api/v1", func(g *web.Router) {
        g.Resource("posts", controllers.NewPostController(app.DB()))
    })
}

Resource() registers six routes in one call:

GET     /api/v1/posts            → Index
GET     /api/v1/posts/{id}       → Show
POST    /api/v1/posts            → Store
PUT     /api/v1/posts/{id}       → Update
PATCH   /api/v1/posts/{id}       → Update
DELETE  /api/v1/posts/{id}       → Destroy

Group is recursive: nest groups for /api/v1/admin/... and pass g.Use(adminOnly) to apply middleware to just that branch.

5. Write main.go

package main

import (
    "log"

    "github.com/devituz/lagodev/config"
    "github.com/devituz/lagodev/database"
    _ "github.com/devituz/lagodev/drivers/sqlite"   // blank import = driver registered
    "github.com/devituz/lagodev/web"

    _ "github.com/you/myapp/migrations"             // registers migrations via init()
    _ "github.com/you/myapp/seeders"                // (optional) seeders too

    appcfg "github.com/you/myapp/config"
    "github.com/you/myapp/routes"
)

func main() {
    _ = config.LoadEnv()

    cfg   := appcfg.App()
    dbCfg := appcfg.Database()

    mgr := database.NewManager()
    conn, err := mgr.Open("default", dbCfg)
    if err != nil { log.Fatal(err) }

    app := web.New(
        web.WithDatabase(conn),
        web.WithManager(mgr),
        web.WithMigrations(nil),     // applies registered migrations at startup
        web.WithAddr(cfg.Addr),
    )
    routes.Register(app)
    app.MustRun()
}

The blank import (_ "myapp/migrations") is what makes init() run — that's where each migration registers itself with the global registry. Forget the underscore and the migrator runs against an empty list. The same pattern applies to seeders.

6. Run it

go run .

The server prints every registered route and starts listening on :8080 (or whatever APP_ADDR you set):

ro'yxatdan o'tgan marshrutlar:
  GET     /health
  GET     /api/v1/posts
  GET     /api/v1/posts/{id}
  POST    /api/v1/posts
  PUT     /api/v1/posts/{id}
  PATCH   /api/v1/posts/{id}
  DELETE  /api/v1/posts/{id}
server tinglamoqda: http://localhost:8080

Try it:

# create
curl -X POST http://localhost:8080/api/v1/posts \
    -H "Content-Type: application/json" \
    -d '{"Title":"Hello","Body":"World"}'
# → 201 Created with the inserted row

# list
curl http://localhost:8080/api/v1/posts
# → 200 OK with the array

# fetch by id
curl http://localhost:8080/api/v1/posts/1
# → 200 OK or 404 if missing (auto-mapped from orm.ErrNotFound)

# delete
curl -X DELETE http://localhost:8080/api/v1/posts/1
# → 204 No Content

Ctrl+C triggers a graceful shutdown — in-flight requests finish before the listener closes (see Web-Framework for the shutdown contract).

Live reload during development

The repo ships an .air.toml config. Install air once:

go install github.com/air-verse/air@latest

Then run air from your project root and every save rebuilds and restarts the server.

7. Write a test

// services/post_service_test.go
package services_test

import (
    "context"
    "testing"

    lagotest "github.com/devituz/lagodev/testing"
    "github.com/you/myapp/services"
)

func TestList(t *testing.T) {
    conn, cleanup := lagotest.SQLite(t)
    defer cleanup()

    svc := services.NewPostService(conn)
    out, err := svc.List(context.Background())
    if err != nil { t.Fatal(err) }
    if len(out) != 0 { t.Fatalf("expected empty, got %d", len(out)) }
}

lagotest.SQLite(t) spins up an in-memory database and applies every migration registered via init(). Tests run in parallel because each gets its own database — no shared state.

8. Where next?

  • Web-Framework — the full Context API, middleware, the secure- by-default stack (security headers, CSRF, rate limit, body limit), validation, cookies.
  • CLI-Reference — every lago command with flag-level detail.
  • ORM — query-builder cookbook, hooks, casts, transactions.
  • Migrations-and-Schema — the full Blueprint DSL, programmatic use, safety guarantees.
  • Factories-and-Seeders — generic factories with states, topological seeders.
  • examples/blog — a more realistic project with three models, foreign keys, and seeders, all wired together.

Clone this wiki locally