DEV Community

Cover image for I Built a Go Project Scaffolding Tool (Because the Ecosystem Needed One)
Aditya
Aditya

Posted on

I Built a Go Project Scaffolding Tool (Because the Ecosystem Needed One)

If you've ever started a new Go project, you know the drill. Create a directory, run go mod init, write a main.go, set up a router, wire up handlers, add a Makefile, configure a .gitignore before you've written a single line of your code, you've already spent 30 minutes on boilerplate.

The JavaScript world has create-react-app. The Java world has Spring Initializr. The Go world had copy-paste from a previous project and hope for the best.

So I built go-initializer — a scaffolding tool that generates a fully-wired, immediately-runnable Go project in seconds. Here's how it works and the decisions I made along the way.


What It Generates

Given a few inputs (project name, module path, project type, framework, addons), go-initializer spits out a zip file containing a production-ready project:

  • go.mod with the right dependencies already declared
  • main.go wired to the chosen framework
  • Handler, router, and server files
  • Makefile, .gitignore, README.md
  • Optional Dockerfile (multi-stage, non-root, Alpine)
  • Optional .env.example (auto-generated based on which secrets your setup needs)

It supports 5 project types — Microservice, API Server, CLI App, Simple Project, and AI Agent — across 14+ frameworks including Gin, Echo, Fiber, Chi, Cobra, gRPC, LangChainGo, OpenAI, Gemini, and Ollama.


Three Ways to Use It

One thing I was deliberate about early on: the tool should meet developers where they are.

1. Interactive CLI (TUI)

goini new
Enter fullscreen mode Exit fullscreen mode

This launches a charmbracelet/huh-powered terminal form wizard. You pick your project type, framework, addons — all in an interactive, keyboard-driven UI that re-renders dynamically as you make selections.

2. Fully scriptable (CI-friendly)

goini new --name myservice --module github.com/me/myservice \
  --type microservice --framework gin --addons redis,zap --docker
Enter fullscreen mode Exit fullscreen mode

All flags provided = no prompts. The tool detects non-TTY environments and skips optional prompts silently. Required fields fail loudly.

3. REST API / Web UI

curl -X POST https://api.goinitializer.com/api/generate \
  -H "Content-Type: application/json" \
  -d '{"name":"myapp","module":"github.com/me/myapp","type":"api-server","framework":"fiber"}' \
  --output myapp.zip
Enter fullscreen mode Exit fullscreen mode

The web UI at goinitializer.com calls the same API.


The Core Architecture Decision: One Shared Generator

The most important design choice I made was ensuring the CLI and the REST API call identical code. There's a GeneratorRegistry — a plain map[string]Generator — where each project type registers its generator implementation.

type Generator interface {
    Generate(ctx context.Context, req *types.CreateProjectRequest) ([]byte, error)
}
Enter fullscreen mode Exit fullscreen mode

The CLI calls GeneratorRegistry["microservice"].Generate(ctx, req). The HTTP handler calls GeneratorRegistry["microservice"].Generate(ctx, req). The output
is always bit-for-bit identical regardless of which surface you used. This meant I could test the generation logic independently of the transport layer and never worry about subtle drift between CLI and API outputs.

Adding a new project type is just: implement the interface, register it in the map.


Generating Go Code Programmatically

Most project templates are generated as raw string literals in switch statements per framework. But for more complex cases — particularly the Golly framework integration and cache addons — I used dave/jennifer, an AST-based Go code generation library.

Jennifer lets you construct Go source files programmatically:

f := jen.NewFile("main")
f.Func().Id("main").Params().Block(
    jen.Id("app").Op(":=").Qual("github.com/go-golly/golly", "New").Call(),
    jen.Id("app").Dot("Run").Call(),
)
Enter fullscreen mode Exit fullscreen mode

This is much safer than string templates for complex code — you can't produce syntactically invalid Go. The tradeoff is verbosity, so I reserved it for cases where the structure was complex enough to justify it.


The AI Agent Scaffolding

The feature I'm most proud of is the AI Agent project type. This isn't just "here's a file that imports the OpenAI SDK." For every supported AI provider, the generator produces:

  • llm/client.go — a typed LLM client
  • agent/agent.go — a complete ReAct-style tool-calling loop
  • tools/tools.go — tool definitions in the correct schema format for each provider (OpenAI's ChatCompletionToolParam, Gemini's genai.FunctionDeclaration, Ollama's JSON schema structs, LangChainGo's tools.Tool interface)

The generated agent actually runs. You scaffold it, add your API key, go run ., and you have a working agent loop with tool-calling — no additional setup.


Security on the Server Side

Since the API is publicly accessible, I spent time on hardening:

Rate limiting: A per-IP token bucket using golang.org/x/time/rate. The rateLimiterStore lazily creates limiters per IP, tracks lastSeen timestamps, and a background goroutine evicts stale entries every 5 minutes (idle >10 minutes).
This prevents unbounded memory growth without a heavy external dependency.

Zip-slip prevention: The CLI's extractZip() uses Go 1.23's os.OpenRoot() to scope all file writes to the output directory. Each entry is capped at 100 MiB to guard against decompression bombs.

Security headers: The server middleware sets X-Content-Type-Options, X-Frame-Options, a restrictive CSP (default-src 'none'), Referrer-Policy, and Permissions-Policy.

Body size cap: Only the /api/generate route gets a MaxBytesReader applied (64 KB cap) to prevent large payload abuse.


The Dynamic TUI

The two-phase form design was one of the more interesting implementation challenges.
Phase 1 asks for name, module, Go version, and project type. Phase 2 asks for framework — but the available framework options depend on what you picked in phase 1.

charmbracelet/huh supports this via OptionsFunc:

huh.NewSelect[string]().
    Title("Framework").
    OptionsFunc(func() []huh.Option[string] {
        return frameworksFor(opts.projectType)
    }, &opts.projectType)
Enter fullscreen mode Exit fullscreen mode

The &opts.projectType dependency pointer tells huh to re-evaluate the options function whenever projectType changes. The form re-renders in place — no restarts, no separate prompts.


What's Next

  • More addons (rate limiting, auth middleware, observability)
  • goini add command to inject addons into an existing project
  • Better Go 1.24 template support

Try It

# Install via Homebrew
brew tap neo7337/goini
brew install goini

# Or grab a binary from GitHub releases
# https://github.com/neo7337/go-initializer/releases
Enter fullscreen mode Exit fullscreen mode

Web UI: goinitializer.com

Source: github.com/neo7337/go-initializer

Feedback and contributions welcome — especially if you work with a framework or addon that isn't supported yet.

Top comments (0)