Skip to content

Latest commit

 

History

495 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Airway

Airway is a full-stack Go framework inspired by Ruby on Rails. It builds server-rendered web apps, JSON APIs, static showcase sites, and native desktop apps from the same application code. It runs against PostgreSQL, MySQL 8 and SQLite — the database driver is inferred from the DSN at runtime.

Features

  • Generics-based repository (lib/repo): typed FindBy[User], CreateFrom[User], preload/eager-loading, joins, transaction-bound helpers.
  • Dialect-aware SQL builder (lib/sql + pg / mysql / sqlite dialects); conditions like sql.Eq, sql.AllOf, sql.Gt; native FOR UPDATE SKIP LOCKED support (sql.ForUpdateSkipLocked).
  • Schema-driven migrations: generate and apply migrations with the CLI; SQLite schema changes are handled by table rebuild.
  • Unified file storage (lib/storage): local directory, Amazon S3, Cloudflare R2 or Tencent COS — selected purely by configuration, exposed via an HTTP upload/download API.
  • Gin web server + WebSocket pub/sub.
  • HTML views with templ: pages as .templ templates under app/views/, rendered from actions via lib/render.HTML.
  • Frontend without Node: npm dependencies managed by the CLI (js:add / js:install, js.pkg.json lock), bundled with an embedded esbuild (js:build), dev-time in-memory rebuilds with livereload, and interactive Preact islands built on the bundled airway-ui component library — all committed and embedded into the single Go binary.
  • OpenAPI 3.2 documentation generated from the running router — operationIds derived from handlers, enriched per module in code, served live at /openapi.json.
  • Admin panel generator: a TOML table spec produces a complete admin backend — authentication, roles, an audit trail, CSV export and server-side lists.
  • Desktop apps: airway desktop:init exports the project as a Wails v3 desktop target; the same web stack runs in a native WebView window on macOS, Windows and Linux.
  • Static showcase sites: airway ssg:new scaffolds a static site whose pages are Go code and whose looks come from swappable theme modules (theme:new / theme:install); airway ssg:build exports plain HTML for any static host.
  • Static export for CDN: airway static:build exports app pages (registered in export.go) as plain HTML plus the frontend bundle — templ-rendered content with Preact islands intact, deployable to any CDN (see docs/static-export.md).
  • Plugins: WordPress-style feature modules shipped as independent Go modules — install with go get, enable with one blank import in plugins.go (see docs/plugin.md).
  • Scaffolding CLI (airway generate ..., db:migrate, ...).
  • Repo REPL with typed scan and Go-expression evaluation.
  • Optional sub-path prefix (URL_PREFIX) for deploying behind a reverse proxy, e.g. http://host:1900/airway/....

Quick start

Requires Go 1.27.1 or later. No Node.js, no CGO.

1. Install the CLI and scaffold a project

go install github.com/daqing/airway@latest
airway new myapp        # or: airway new github.com/me/myapp (directory = last path segment)
                        # or: airway new /path/to/myapp (path; module = last path segment)
                        # relative paths work the same: airway new sites/myapp
cd myapp

airway new generates a fresh project skeleton from the framework's app/ scaffold, seeds .env from .env.example, runs go mod tidy, installs the frontend dependencies (airway js:install), and prints the follow-up steps. To hack on the framework itself instead, clone the repository:

git clone https://github.com/daqing/airway.git
cd airway
cp .env.example .env

Open .env and set at least the database DSN and listen address:

DSN="sqlite://./tmp/airway.db"     # or postgres://..., mysql://...
LISTEN=":1900"                     # host:port to bind; ":1900" is every interface

Environment values may use the short name (DSN, LISTEN, REDIS, URL_PREFIX) or the AIRWAY_-prefixed form (AIRWAY_DSN, AIRWAY_LISTEN, ...). When both are set, the AIRWAY_ form wins. See Configuration.

2. Run the server

go run . server   # or: airway server (requires AIRWAY_ENV, e.g. AIRWAY_ENV=local, and a configured DSN)

Or start the local dev server with live reload:

just dev

The app listens on http://127.0.0.1:1900 (GET / serves an HTML page rendered with templ, GET /health returns UP).

Configuration

All configuration is via environment variables (see .env.example). Values in .env are fallbacks: the process environment always wins (e.g. LISTEN=0.0.0.0:1988 airway server overrides a LISTEN in .env). Each value accepts a short name or its AIRWAY_ alias, with the alias taking precedence.

Variable Description
DSN / AIRWAY_DSN Database URL. Driver is inferred from the scheme (see below).
LISTEN / AIRWAY_LISTEN Address the server binds, host:port (e.g. 0.0.0.0:1905, :1905). Default :1900.
REDIS / AIRWAY_REDIS Optional Redis URL for cache/queue.
URL_PREFIX / AIRWAY_URL_PREFIX Optional public sub-path prefix, e.g. /airway. Empty serves at the root.
AIRWAY_JS_REGISTRY npm registry for js:add / js:install (default https://registry.npmjs.org; set a mirror like https://registry.npmmirror.com if needed).
AIRWAY_ENV local loads .env and uses Gin debug mode; anything else runs release mode.
STORAGE_DRIVER local (default), s3, r2 or cos.
STORAGE_ROOT Local storage root (default ./data/storage).
STORAGE_* Cloud settings: STORAGE_REGION, STORAGE_ENDPOINT, STORAGE_BUCKET, STORAGE_ACCESS_KEY, STORAGE_SECRET_KEY, STORAGE_PUBLIC_URL (optional CDN base).
TZ Server timezone, e.g. Asia/Shanghai.

Database DSNs

The same code switches databases by changing only DSN:

# PostgreSQL
DSN="postgres://daqing:passwd@127.0.0.1:5432/airway"

# SQLite (file or in-memory) — pure-Go driver, no CGO required
DSN="sqlite://./tmp/airway.db"
DSN="sqlite://:memory:"

# MySQL 8 (URL or native driver format)
DSN="mysql://root:passwd@127.0.0.1:3306/airway?charset=utf8mb4"
DSN="root:passwd@tcp(127.0.0.1:3306)/airway?charset=utf8mb4&parseTime=true"

Notes:

  • Basic CRUD flows are portable across PostgreSQL, MySQL 8 and SQLite.
  • Some advanced SQL Builder helpers (ARRAY, JSONB, a few lateral/window expressions) are still PostgreSQL-oriented.
  • SQLite support uses the pure-Go modernc.org/sqlite driver.

HTTP endpoints

Registered in config/routes.go:

Method Path Description
GET / Home page (HTML, rendered from a templ view).
GET /ui airway-ui component showcase (interactive island).
GET /openapi.json The live OpenAPI 3.2 document (see API documentation).
GET /health Health check.
GET /ws WebSocket connection.
POST /ws/publish Publish a message to connected clients (form field message).
POST /api/v1/storage Upload a file (multipart file, optional dir).
GET /api/v1/storage/*key Download a file.
DELETE /api/v1/storage/*key Delete a file.
GET /api/v1/ui-demo/items Demo data for the TanStack island on /ui.

When URL_PREFIX is set, the public routes — home page, WebSocket, and API — are served only under that prefix, not at the root. The health check also stays reachable at the bare root, so load-balancer probes can hit /health without the prefix. For example with URL_PREFIX="/airway":

curl http://127.0.0.1:1900/airway/health
curl http://127.0.0.1:1900/health          # still answers (probe)
curl -F "file=@report.pdf" http://127.0.0.1:1900/airway/api/v1/storage

Clients — including WebSocket connections — must include the prefix (ws://host:1900/airway/ws). Local storage URLs returned by the API are prefixed accordingly; cloud/CDN URLs are untouched.

HTML views (templ)

Pages are templ templates under app/views/, one folder per API module — the folder name drops the _api suffix, so home_api renders app/views/home/index.templ. Each folder is its own package; a shared document shell lives in app/views/layouts/base.templ. Actions serve a component with the lib/render HTML helper:

// app/api/home_api/index_action.go
render.HTML(c, home.Index())

After editing any .templ file, regenerate the Go code and keep it committed:

go generate ./...   # or: just generate, or: airway templates:compile

The generated *_templ.go files are committed, so building and testing never require the templ CLI.

Interactive islands

Pages stay server-rendered; interactive regions are islands — Preact TSX components mounted on data-island nodes with server-provided props:

// in a .templ view (see app/views/home for a live example)
@assets.Island("counter", map[string]any{"start": 3})

The component lives at app/assets/js/islands/counter.tsx (default-export it; the file path is the island name) and is bundled automatically — no manual registration. In local development the bundle rebuilds in memory and the browser reloads on change; in production it is embedded in the binary behind a cache-busted URL. Pages render completely without JavaScript: the mount point just stays empty.

airway-ui is the bundled component library islands build on: buttons, inputs, forms (react-hook-form), tables (TanStack Table), modals, toasts, tabs and a fetch layer aligned with lib/render's JSON envelope — visual layer self-made, logic layers from the preact/compat ecosystem. See it all live at /ui on a running server.

Frontend strategy

Status: implemented end to end — see the frontend guide. The pipeline ships in the CLI (js:add/js:install/js:build, generate island/scaffold), the project template, and this repository itself (the homepage counter and the /ui component showcase are islands).

Frontend code lives in the same repository as the Go code, gets a component-based workflow comparable to a modern UI framework, and does not introduce a Node.js toolchain:

  • templ renders the skeleton — page structure, SEO, first paint.
  • Interactive regions are islands — Preact TSX components under app/assets/js/, mounted on elements marked with data-island; initial data is serialized next to the mount point.
  • esbuild embedded as a Go library — the CLI links github.com/evanw/esbuild/pkg/api directly: airway js:build compiles TS/TSX, the dev server serves rebuilt bundles from memory, and production bundles are embedded into the single Go binary via go:embed.
  • A home-grown airway-ui component library on a preact/compat base, so logic-heavy React-ecosystem libraries (TanStack Table/Query/Form, React Hook Form) stay usable while the visual layer stays self-made.

Rejected alternatives:

  • htmx + Alpine.js (HTML over the wire) — fine for progressive enhancement, but it offers no component-based reactive programming model; interactivity caps out well short of a real component library.
  • A Node frontend sub-project (Vue, React/Next.js, Svelte, ...) embedded via go:embed — drags a full Node toolchain into the repository; at that point a real frontend/backend split is the more honest architecture.
  • LiveView-style server-driven UI — little practical value for a Go framework; when an application genuinely needs heavy frontend engineering, splitting the frontend out is the right answer.

Escape hatch: applications that outgrow islands (complex SPAs, rich editors) should split the frontend into its own project (Vue, React, Svelte, ...) and consume Airway purely as a JSON API.

API documentation (OpenAPI)

Every API route served by the binary is documented as an OpenAPI 3.2 document — no annotations required. airway openapi:generate scans the router, derives operationIds from the handler names, and writes ./openapi.json (a build artifact, git-ignored — regenerate it whenever routes change). The same document is served live at /openapi.json.

Modules enrich their documentation in code with an openapi.go file declaring operations via lib/openapi (openapi.Get(...) etc.), including request/response schemas inferred from Go types; document-level metadata lives in app/api/openapi_api/doc.go. See the OpenAPI guide.

Admin panel

airway admin:generate reads a TOML table spec (config/admin.toml) and generates a complete, production-ready admin backend as real, user-owned Go code: cookie-session authentication, roles, an audit trail, CSV export, and server-side lists — with type-aware forms and filters for every declared field (datetime, enum, references, attachment included).

airway admin:generate                    # or: admin:generate --force=table1,table2
airway admin:root admin 's3cret'         # create the administrator account
airway admin:member editor 's3cret'      # non-admin panel accounts (--role=editor|viewer)
airway server                            # sign in at /admin/login

See the admin panel guide (中文).

Desktop apps

airway desktop:init exports the project as a Wails v3 desktop target in ./desktop: the same web stack (templ views, islands, JSON APIs, WebSocket) runs on a local loopback port inside the desktop process, and a native WebView window loads it — server-rendered pages, cookie sessions, redirects and WebSockets behave exactly as on the web, with no application code changes. SQL migrations ship embedded and apply automatically on launch; packaging covers macOS (.app), Windows (NSIS) and Linux (deb/rpm/AppImage).

airway desktop:init       # generate ./desktop; re-run to re-sync migrations/plugins

See the desktop guide (中文) and the research record in WAILS.md.

Static showcase sites (SSG)

Airway doubles as a static site generator for showcase websites — company homepages, product landings, portfolios. airway ssg:new scaffolds a site project whose pages are declared in Go (ssg.go) against a swappable theme module; airway ssg:build exports a plain HTML directory for any static host, and airway ssg:serve previews it locally. Themes are ordinary Go modules (templ components + embedded assets): scaffold one with airway theme:new, and install one into a site with airway theme:install. The framework bundles a corporate reference theme.

airway ssg:new mysite       # then: airway ssg:build / airway ssg:serve

See SSG.md (中文) for the design record and docs/ssg.md (中文) for the usage guide.

CLI

The Airway CLI is a single airway binary (install with go install github.com/daqing/airway@latest). Inside a project it detects the host application and transparently re-runs every project-scoped command through go run . (stderr shows a proxying to project binary notice), so plugins, REPL models and Go-code migrations always come from the project's own binary. Commands auto-load .env from the project root.

Project and server

airway new <module-path | /path>            # scaffold a new project skeleton
airway server                               # start the HTTP server
airway generate api admin                   # new API namespace under app/api/
airway generate action admin show           # new action in an existing API module
airway generate model post                  # new model in app/models/
airway generate service post title:string   # CRUD service in app/services/
airway generate island chart                # interactive island component
airway generate scaffold post title:string  # full CRUD: model+migration+API+page+island
airway generate migration create_posts      # new .up.sql/.down.sql pair in db/migrate/
airway repl                                 # interactive repo REPL (proxied to go run . in projects)
airway version                              # print version (also -v, --version)

Database

airway db:create                            # create the database
airway db:drop                              # drop the database
airway db:migrate [version]                 # apply migrations
airway db:rollback [step]                   # roll back migrations
airway db:status                            # migration status
airway schema:dump                          # write db/schema.json
airway schema:show                          # print db/schema.json

Frontend

airway js:add <pkg>[@version]               # add a frontend npm dependency (no Node required)
airway js:install                           # install js.pkg.json deps into app/assets/js/vendor/
airway js:build                             # bundle app/assets/js into app/assets/dist (esbuild)
airway templates:compile                    # regenerate the templ views (shorthand for `go generate ./...`)

API documentation

airway openapi:generate [--out path]        # write the OpenAPI 3.2 document (default ./openapi.json)

Admin panel

airway admin:generate [config/admin.toml]   # generate the admin backend from a TOML table spec
airway admin:root <username> <password>     # create the administrator account (role admin)
airway admin:member <username> <password> [--role=editor|viewer]
                                            # create a non-admin panel account

Desktop apps

airway desktop:init [--force]               # generate the Wails v3 desktop target in ./desktop

Static sites and themes

airway ssg:new [--local[=path]] <name>      # scaffold a static showcase site project
airway ssg:build [--out dist]               # export the site defined in ssg.go as static HTML
airway ssg:serve [--addr 127.0.0.1:3000]    # preview the site with a local server
airway theme:new [--local[=path]] <name>    # scaffold a new site theme module
airway theme:install <module | /path>       # install a site theme into the host project

Static export (app pages)

airway static:build [--out dist]            # export the pages registered in export.go + the frontend bundle as static HTML
airway static:serve [--addr 127.0.0.1:3000] # preview the static pages with a local server

Plugins

airway plugin:new <module-path | /path>     # scaffold a new plugin module
airway plugin:list                          # registered plugins and mount paths
airway plugin:install <module>              # enable a plugin + install its SQL migrations and deps/
airway plugin:lint                          # check the current plugin project for legacy layout issues

Files

airway upload [key] /path/to/file           # upload via the configured storage

Database commands read DSN/AIRWAY_DSN. See the full CLI guide.

Repository API (lib/repo)

Models

A model is a struct with db tags and a TableName() method. Associations are declared via Relations().

import "github.com/daqing/airway/lib/repo"

type User struct {
	ID      int64    `db:"id"`
	Name    string   `db:"name"`
	Email   string   `db:"email"`
	Profile *Profile // belongs_to
	Posts   []*Post  // has_many
}

func (User) TableName() string { return "users" }

func (User) Relations() map[string]repo.Relation {
	return map[string]repo.Relation{
		"Profile": repo.HasOne(Profile{}, "UserID"),
		"Posts":   repo.HasMany(Post{}, "UserID"),
	}
}

type Post struct {
	ID     int64  `db:"id"`
	UserID int64  `db:"user_id"`
	Title  string `db:"title"`
	Author *User  // belongs_to
}

func (Post) TableName() string { return "posts" }

func (Post) Relations() map[string]repo.Relation {
	return map[string]repo.Relation{
		"Author": repo.NewBelongsTo(User{}, "UserID"),
	}
}

CRUD

All helpers use the DB configured at boot (repo.SetupDB) and take the model type as a type parameter:

// Create
user, err := repo.CreateFrom[User](sql.H{"name": "John", "email": "john@example.com"})

// Read
user, err := repo.FindByID[User](1)
user, err := repo.FindOneBy[User](sql.H{"email": "john@example.com"})
users, err := repo.FindBy[User](sql.H{"active": true})
users, err := repo.FindAll[User]()

// Update
err := repo.UpdateByID[User](1, sql.H{"name": "Jane"})
err := repo.UpdateWhere[User](sql.H{"status": "inactive"}, sql.Eq("last_login_at", nil))

// Delete
err := repo.DeleteByID[User](1)
err := repo.DeleteWhere[User](sql.H{"status": "banned"})

// Exists / Count
ok, err := repo.ExistsWhere[User](sql.H{"email": "john@example.com"})
n, err := repo.CountWhere[User](sql.H{"active": true})
n, err := repo.CountEvery[User]()

Transactions

repo.WithTx runs a callback on a single transaction-bound connection. The callback receives a *repo.Tx whose helper methods execute on that transaction; the generic helpers take tx.Executor() via the *With variants:

users := sql.TableOf("users")

err := repo.WithTx(db, func(tx *repo.Tx) error {
	if _, err := repo.InsertWith[User](tx.Executor(), sql.Insert(sql.H{"name": "John"}).IntoTable(users)); err != nil {
		return err
	}

	n, err := tx.Count(sql.SelectColumns("count(*)").FromTable(users))
	if err != nil {
		return err
	}

	return nil // commit; a non-nil return rolls back
})

Use tx.Raw() to drop down to *sql.Tx for hand-written SQL. There is no public Commit/Rollback — the outcome follows the callback's return value, and repo.WithTxContext accepts a context. JoinQuery/Preloader remain pool-only and cannot run inside a transaction.

Optimistic locking recipe: UpdateAffected with a version check, so exactly one concurrent writer wins:

affected, err := tx.UpdateAffected(
	sql.UpdateTable(users).
		Set(sql.H{"name": "Jane", "version": sql.Expr("version + 1")}).
		Where(sql.AllOf(sql.Eq("id", 1), sql.Eq("version", currentVersion))),
)
if err != nil {
	return err
}
if affected == 0 {
	return errors.New("stale record")
}

PostgreSQL is the primary target for transactions. The MySQL insert path issues a second lookup statement on the same connection, which is best-effort inside a transaction.

Preload (eager loading)

Preload replaces N+1 loops with a couple of queries:

users, _ := repo.FindBy[User](sql.H{})
err := repo.Preload("Profile", "Posts").Exec(&users)

// Nested and conditional
err := repo.Preload("Posts").ThenPreload("Comments").Exec(&users)
err := repo.PreloadCond("Posts", sql.AllOf(
	sql.Eq("published", true),
	sql.Gte("created_at", "2024-01-01"),
)).Exec(&users)

Joins

results, err := repo.Join(User{}).LeftJoins("Profile").Find()
results, err := repo.Join(User{}).
	Joins("Posts", sql.Gt("posts.views", 100)).
	Where(sql.Eq("users.active", true)).
	OrderBy("users.name ASC").
	Page(1, 20).
	Find()

count, err := repo.Join(User{}).Joins("Posts").Count()

var users []*User
err := repo.Join(User{}).LeftJoins("Profile").FindInto(&users)

Rails mapping

Rails ActiveRecord Airway
User.find(id) repo.FindByID[User](id)
User.find_by(email: e) repo.FindOneBy[User](sql.H{"email": e})
User.where(active: true) repo.FindBy[User](sql.H{"active": true})
User.all repo.FindAll[User]()
User.create(attrs) repo.CreateFrom[User](attrs)
User.update(id, attrs) repo.UpdateByID[User](id, attrs)
User.delete(id) repo.DeleteByID[User](id)
User.joins(:profile) repo.Join(User{}).Joins("Profile")
User.includes(:posts) repo.Preload("Posts").Exec(&users)
User.count repo.CountEvery[User]()
User.where(active: true).count repo.CountWhere[User](sql.H{"active": true})

Repo REPL

Exercise lib/repo directly against the configured database:

go run . repl                              # uses the configured DSN
go run . repl --driver sqlite --dsn ./tmp/airway.db

The REPL only sees the models compiled into the binary it runs in (registered via lib/replreg). Inside a project, airway repl proxies to go run . repl automatically, so your project's models show up; outside a project only the framework's built-in models are visible.

Commands: help, driver, tables, exit. Type a Go expression to evaluate it — builders print the compiled SQL, repo.* calls run against the database:

repo.FindOne("posts", sql.Eq("id", 1))
repo.Find[models.Post](pg.Select("id").Where(sql.Eq("id", 1)))
repo.Insert[models.Post](pg.H{"id": 1234, "title": "hello"})
repo.Update("posts", pg.H{"published": false}, sql.Eq("id", 1))
repo.Delete("posts", sql.Eq("id", 1))
pg.Select("*").From("posts").Where(sql.Eq("id", 1))

Available namespaces: repo, sql, pg, mysql, sqlite, models. repo.Find/FindOne/Count/Exists accept a built statement or a table + condition; typed calls support anonymous structs and app models. Full-table updates/deletes require an explicit true as the last argument.

File storage

lib/storage is a unified layer over a local directory or a cloud backend (S3 / R2 / COS). Configure the backend with STORAGE_DRIVER:

# Local
STORAGE_DRIVER="local"
STORAGE_ROOT="./data/storage"

# Cloud (S3-compatible): choose s3 / r2 / cos
STORAGE_DRIVER="s3"
STORAGE_REGION="us-east-1"
STORAGE_BUCKET="my-bucket"
STORAGE_ACCESS_KEY="..."
STORAGE_SECRET_KEY="..."
# STORAGE_PUBLIC_URL="https://cdn.example.com"   # optional CDN base

Use it in code via the current backend:

store := storage.Current() // installed at boot by storage.Setup

err := store.Put(ctx, "docs/report.pdf", storage.Object{
	Reader:      file,
	Size:        size,
	ContentType: "application/pdf",
})
rc, err := store.Get(ctx, "docs/report.pdf") // close when done
ok, err := store.Exists(ctx, "docs/report.pdf")
url, err := store.URL(ctx, "docs/report.pdf", 24*time.Hour)
err := store.Delete(ctx, "docs/report.pdf")

URL() returns the app's own download path for the local backend and a CDN or presigned URL for cloud backends. The app exposes a REST API over the same layer:

# Upload (multipart field "file", optional "dir")
curl -F "file=@report.pdf" -F "dir=docs" http://127.0.0.1:1900/api/v1/storage
# => {"key":"docs/202609/ab12cd....pdf","url":"/api/v1/storage/docs/202609/ab12cd....pdf","size":12345}

# Download / delete
curl -O http://127.0.0.1:1900/api/v1/storage/docs/202609/ab12cd....pdf
curl -X DELETE http://127.0.0.1:1900/api/v1/storage/docs/202609/ab12cd....pdf

See the full storage guide.

Deployment

The binary is self-contained. Build a pure-Go image (no CGO toolchain) with:

just docker        # or: docker build -t airway .
docker run -p 1900:1900 -e AIRWAY_ENV=production -e DSN="sqlite:///app/tmp/airway.db" airway

The Dockerfile pulls base images from a daocloud.io mirror and points the Go module proxy at goproxy.cn, so builds work on networks where the default endpoints are slow or blocked; strip the mirrors if you don't need them.

Run migrations before/after deploy:

./airway db:migrate

See docs/docker-compose.yml.example for a compose example. To serve the app under a path prefix behind a reverse proxy, set URL_PREFIX (e.g. /airway) — see HTTP endpoints.

About

Full-stack Web framework written in Go, inspired by Ruby on Rails.

Topics

Resources

Stars

60 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages