$ git clone https://github.com/angvp/tango $ cd tango/examples/notes-starter $ go run .
Notes starter
This example is a small tanGO app you could keep as a starting point: one model, JSON routes, generated migrations, the HTML admin, and ratelimit.Middleware on the note-creation route.
Run locally
git clone https://github.com/angvp/tango
cd tango/examples/notes-starter
go run . -check
go run . -migrate
go run . -tango-admin-create=admin
go run .
-tango-admin-create prompts for a password on stdin and creates the account with a bcrypt-hashed password — there is no static credential to set in .env. Open http://localhost:8000/admin/ and log in with that username/password; you'll land on /admin/login/ first since admin auth is a real session-cookie login, not HTTP Basic Auth.
POST /api/notes/ is rate limited per client IP (5 requests, refilling one every 10 seconds, via ratelimit.NewLimiter/ratelimit.Middleware in main.go) — exceeding it returns 429 with a Retry-After header. GET /api/notes/ is not limited. See rate limiting for the full model; this example's limits are deliberately small so you can trigger the 429 locally without waiting.
Configuration
The address is TANGO_ADDR, else the PORT hosting platforms set, else :8000. Ctrl-C or SIGTERM shuts the server down gracefully.
TANGO_DB_DSN picks the database by its scheme and defaults to sqlite://app.db (a file in the working directory). sqlite:///var/data/app.db is an absolute path and sqlite://:memory: an in-memory database.
To adapt this example to Postgres, swap the modernc.org/sqlite driver import for github.com/jackc/pgx/v5/stdlib and set TANGO_DB_DSN=postgres://user:password@host:5432/dbname; the dialect and driver name come from the DSN. New projects can generate that shape directly with tango newproject --dialect=postgres.
examples/notes-starter/main.go
packagemainimport("context""database/sql""errors""fmt""net/http""os""os/signal""syscall""time""github.com/angvp/tango""github.com/angvp/tango/admin""github.com/angvp/tango/db""github.com/angvp/tango/model""github.com/angvp/tango/ratelimit""notes-starter/migrations"_"modernc.org/sqlite")// createLimiter throttles note creation per client IP: 5 notes, refilling// one every 10 seconds. A real app would tune this to its own traffic// shape — see docs/guides/rate-limiting.md.varcreateLimiter=mustNewCreateLimiter()funcmustNewCreateLimiter()*ratelimit.Limiter{limiter,err:=ratelimit.NewLimiter(ratelimit.Options{Limit:5,Refill:10*time.Second})iferr!=nil{panic(err)}returnlimiter}typeNotestruct{IDint64`tango:"pk"`TitlestringBodystringPrivatebool}typeNotesAppstruct{store*db.Storemetamodel.ModelMeta}func(a*NotesApp)Name()string{return"notes"}func(a*NotesApp)Register(registry*tango.Registry)error{iferr:=registry.Models().Register(Note{});err!=nil{returnerr}meta,ok:=registry.Models().Get("Note")if!ok{returnerrors.New("Note model is not registered")}a.meta=metaiferr:=registry.Admin().Register(Note{},admin.Options{ListDisplay:[]string{"Title","Private"},Search:[]string{"Title","Body"},Ordering:[]string{"Title"},});err!=nil{returnerr}returnregistry.Routes().Include("/api/notes/",tango.URLs{tango.Path("GET","/",a.list,tango.Name("list")),tango.Path("POST","/",a.create,tango.Name("create"),tango.Use(ratelimit.Middleware(createLimiter,ratelimit.RemoteIPKey()))),})}func(a*NotesApp)list(ctx*tango.Context)error{varnotes[]Noteiferr:=a.store.List(ctx.Context(),a.meta,db.Query{Limit:50,OrderBy:[]string{"Title"}},¬es);err!=nil{returnerr}returnctx.JSON(200,notes)}func(a*NotesApp)create(ctx*tango.Context)error{varnoteNoteiferr:=ctx.Bind(¬e);err!=nil{returnerr}iferr:=a.store.Create(ctx.Context(),a.meta,¬e);err!=nil{returnerr}returnctx.JSON(201,note)}funcmain(){iferr:=run();err!=nil{fmt.Fprintln(os.Stderr,err)os.Exit(1)}}funcrun()error{iferr:=tango.LoadEnvFile(".env");err!=nil{returnerr}dsn,err:=tango.LoadDBConfigFromEnv()iferr!=nil{returnerr}sqlDB,err:=sql.Open(dsn.Driver,dsn.Source)iferr!=nil{returnerr}defersqlDB.Close()store:=db.NewStore(sqlDB,dsn.Dialect)config:=appConfig(store)ifhandled,err:=admin.HandleCLI(context.Background(),store,os.Args[1:],os.Stdin,os.Stdout,os.Stderr);handled||err!=nil{returnerr}handled,err:=tango.DispatchFlags(config,sqlDB,dsn.Dialect,migrations.Migrations)ifhandled||err!=nil{returnerr}// Ctrl-C or SIGTERM cancels ctx, and ServeContext shuts down gracefully.ctx,stop:=signal.NotifyContext(context.Background(),os.Interrupt,syscall.SIGTERM)deferstop()fmt.Println("listening on",config.Addr)returntango.ServeContext(ctx,config,sqlDB,dsn.Dialect)}funcappConfig(store*db.Store)tango.Config{// The address is TANGO_ADDR, else the PORT hosting platforms set, else :8000.config:=tango.LoadConfigFromEnv(tango.WithPortFromEnv())config.InstalledApps=[]tango.App{&NotesApp{store:store},admin.New(store),}returnconfig}// buildHandler compiles config into a servable http.Handler exactly the// way tango.Serve does (including wiring store into the registry), split// out here so tests can exercise real routing/middleware (including// createLimiter) against an httptest server without going through main's// flag/env/serve plumbing.funcbuildHandler(configtango.Config,store*db.Store)(http.Handler,error){registry,err:=tango.BuildRegistry(config)iferr!=nil{returnnil,err}iferr:=registry.RunRegistration();err!=nil{returnnil,err}registry.SetStore(store)returnregistry.Routes().Handler()}