Examples

api-with-admin

A JSON API and HTML admin sharing models, with foreign keys

Browse on GitHub
$ git clone https://github.com/angvp/tango
$ cd tango/examples/api-with-admin
$ go run .

API with admin

A JSON API and the HTML admin over one set of models. authors is managed only in the admin; posts has the admin and a JSON API, and every post belongs to an author through a foreign key. It's the compact proof of tanGO's app composition: two apps, one registry, generated migrations.

Run it

git clone https://github.com/angvp/tango
cd tango/examples/api-with-admin
go run . -migrate
go run . -tango-admin-create=admin
go run .

-tango-admin-create prompts for a password. Open http://localhost:8000/admin/, log in, and add an author. Then use the API with that author's ID:

curl -X POST http://localhost:8000/posts/ \
  -H 'Content-Type: application/json' \
  -d '{"Title":"Hello","Body":"First post","AuthorID":1}'

curl 'http://localhost:8000/posts/?author_id=1'

go test . runs the same flow against a fresh database.

The operator dashboard

Logging in to /admin/ lands on a dashboard: the database's reachability, dialect and migration state, and the last hour of request and job activity. Both come from sources main.go hands to admin.WithDashboard: a status function built from tango.Status (see dashboard.go), and an observability.Window that tango.WithRecorder feeds on every request. The activity is labelled "this instance only" and starts again from zero when the process restarts; use an external monitoring system for history or several instances. See the admin dashboard guide.

Configuration

  • TANGO_DB_DSN picks the database and defaults to sqlite://app.db.
  • The address is TANGO_ADDR, else the PORT hosting platforms set, else :8000.
  • Ctrl-C or SIGTERM shuts the server down gracefully.

See the routing guide and admin registration.

examples/api-with-admin/main.go
// Command api-with-admin is a JSON API and the admin over one set of
// models: authors (admin only) and posts (admin and a JSON API), linked by
// a foreign key.
package main

import (
	"context"
	"database/sql"
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/angvp/tango"
	"github.com/angvp/tango/admin"
	"github.com/angvp/tango/db"
	"github.com/angvp/tango/observability"

	"api-with-admin/apps/authors"
	"api-with-admin/apps/posts"
	"api-with-admin/migrations"

	_ "modernc.org/sqlite"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run() error {
	dsn, err := tango.LoadDBConfigFromEnv()
	if err != nil {
		return err
	}
	sqlDB, err := sql.Open(dsn.Driver, dsn.Source)
	if err != nil {
		return err
	}
	defer sqlDB.Close()

	store := db.NewStore(sqlDB, dsn.Dialect)
	// The dashboard's activity numbers come from this Window, which also
	// receives every framework metric; Multi would fan out to an exporter too.
	window := observability.NewWindow()
	config := appConfig(store, sqlDB, dsn.Dialect, window)

	if handled, err := admin.HandleCLI(context.Background(), store, os.Args[1:], os.Stdin, os.Stdout, os.Stderr); handled || err != nil {
		return err
	}
	if handled, err := tango.DispatchFlags(config, sqlDB, dsn.Dialect, migrations.Migrations); handled || err != nil {
		return err
	}

	// Ctrl-C or SIGTERM cancels ctx, and ServeContext shuts down gracefully.
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	fmt.Println("listening on", config.Addr)
	return tango.ServeContext(ctx, config, sqlDB, dsn.Dialect,
		tango.WithRecorder(observability.Multi(window)))
}

// appConfig is the whole application; the tests build the same one.
func appConfig(store *db.Store, sqlDB *sql.DB, dialect db.Dialect, window *observability.Window) tango.Config {
	// The address is TANGO_ADDR, else the PORT hosting platforms set, else :8000.
	config := tango.LoadConfigFromEnv(tango.WithPortFromEnv())
	// The status provider needs the finished config (tango.Status checks
	// registration), so it reads it through this closure.
	var finished tango.Config
	dashboard := admin.DashboardConfig{
		Status:   dashboardStatus(func() tango.Config { return finished }, sqlDB, dialect),
		Activity: window,
	}
	config.InstalledApps = []tango.App{
		posts.New(store),
		authors.New(),
		admin.New(store, admin.WithDashboard(dashboard)),
	}
	finished = config
	return config
}