DEV Community

Cover image for I got tired of setting up Go projects from scratch, so I built a scaffolding CLI
Alexey
Alexey

Posted on

I got tired of setting up Go projects from scratch, so I built a scaffolding CLI

Before I wrote any actual business logic in my last project, I spent days just setting up the infrastructure around it. Parsing configs, writing Dockerfiles, wiring up Compose, connecting Postgres and Redis, getting the server to actually start. At some point I just gave up and went to play Dota instead.

I'd done this before in .NET — dotnet new generates a working project from a template in seconds. Go has nothing like that out of the box. So I built it.

It's called gofro.


What it does

One command:

gofro new myapi --postgres --redis --grafana --github johndoe --git
Enter fullscreen mode Exit fullscreen mode

You get:

  • cmd/main.go with graceful shutdown wired up
  • Config loading via koanf (TOML + env vars, env wins)
  • Docker Compose with only the services you asked for
  • Multi-stage Dockerfile
  • pgxpool connection + goose migrations runner (if --postgres)
  • go-redis client (if --redis)
  • Prometheus scrape config + Grafana in Compose (if --grafana)
  • OpenAPI spec + oapi-codegen setup so you can define your API and generate typed handlers
  • Generic storage layer — GetAll, GetOne, Create, Update, Delete over pgx
  • Module path set to github.com/johndoe/myapi automatically
  • git init if you pass --git

Then go mod tidy and docker compose up -d and you're writing actual code.


Why I built this instead of using something existing

Coming from .NET, dotnet new was just there. Pick a template, get a project, start working. In Go I kept doing the same dance every time: copy the config parser from the last project, rewrite the Compose file, remember how pgxpool initialization works, set up graceful shutdown again.

The last time I did it was for a college logistics platform. I had a tight deadline, hadn't touched Go in a while after a stressful hackathon, and spent way too long just building the scaffolding before writing a single handler. That's when I decided to just solve it once.

gofro is opinionated. It picks the stack for you — chi for routing, koanf for config, pgx for Postgres, goose for migrations, oapi-codegen for API generation. If you want something different, it's probably not for you. But if you're fine with that stack, you skip a day of setup.


The flags

--postgres    pgxpool + goose migrations + generic storage layer
--redis       go-redis/v9 client
--prometheus  Prometheus scrape config
--grafana     Grafana in Compose (enables --prometheus automatically)
--github      sets module path to github.com/<nick>/<project>
--module      full custom module path
--git         runs git init
Enter fullscreen mode Exit fullscreen mode

You can mix and match. Minimal API with no databases:

gofro new myapi --github johndoe
Enter fullscreen mode Exit fullscreen mode

Full stack with observability:

gofro new myapi --postgres --redis --prometheus --grafana --github johndoe --git
Enter fullscreen mode Exit fullscreen mode

What the generated project looks like

myapi/
├── cmd/
│   └── main.go              # graceful shutdown, signal handling, dependency wiring
├── configs/
│   ├── config.toml          # base config
│   └── prometheus.yml       # only with --prometheus
├── internal/
│   ├── api/
│   │   ├── api.swagger.yaml # define your endpoints here
│   │   ├── gen.go           # go:generate directive for oapi-codegen
│   │   └── oapi-codegen.yaml
│   ├── config/
│   │   └── config.go        # struct + koanf loader + DSN helpers
│   ├── database/
│   │   ├── postgres.go      # only with --postgres
│   │   └── redis.go         # only with --redis
│   └── handler/
│       └── server_impl.go   # Server struct, JSON(), Run()
├── pkg/
│   └── storage/
│       └── storage.go       # only with --postgres
├── migrations/              # only with --postgres
├── docker-compose.yml
├── Dockerfile
├── .env
└── Makefile
Enter fullscreen mode Exit fullscreen mode

The workflow after generation:

  1. Define your API in internal/api/api.swagger.yaml
  2. Run make generate — oapi-codegen produces typed models and the server interface
  3. Implement the interface methods on the Server struct
  4. Run docker compose up -d and go run ./cmd/

The generic storage layer

This is the part I use in every project. It's built on go-sqlbuilder and pgx, works with any struct via generics:

// SELECT with optional filter
users, err := storage.GetAll[User](ctx, "users", db,
    func(sb *sqlbuilder.SelectBuilder) {
        sb.Where(sb.Equal("active", true))
    },
)

// SELECT one
user, err := storage.GetOne[User](ctx, db, "users",
    func(sb *sqlbuilder.SelectBuilder) {
        sb.Where(sb.Equal("id", id))
    },
)
if errors.Is(err, storage.ErrNotFound) { ... }

// INSERT
err = storage.Create(ctx, "users", newUser, db)

// UPDATE — skips fields tagged `immutable` (like created_at)
err = storage.Update(ctx, "users", user, db,
    func(sb *sqlbuilder.UpdateBuilder) {
        sb.Where(sb.Equal("id", user.ID))
    },
)
Enter fullscreen mode Exit fullscreen mode

Fields tagged db:"-" are skipped on insert. Fields tagged immutable are skipped on update. No magic, just generics and struct tags.


Install

go install github.com/anxi0uz/gofro@latest
Enter fullscreen mode Exit fullscreen mode

Make sure $(go env GOPATH)/bin is in your $PATH. Then:

gofro new myproject --postgres --redis --github yourname --git
cd myproject
Enter fullscreen mode Exit fullscreen mode

Source: github.com/anxi0uz/gofro

Top comments (2)

Collapse
 
hitler_homeless_0255061af profile image
Hitler Homeless

Very useful! 👍 genius 😎 kkk is proud of you

Collapse
 
kushal1o1 profile image
KUSHAL BARAL

Templates lets us Fast Setup and best part is refining it later on :)