Skip to content

esrid/gogen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

gogen

Warning

This project is experimental. Use it at your own risk.

Rails-style Go project generator. Generates hexagonal architecture projects with a single command.

Install

go install github.com/esrid/gogen@latest

Requires Go 1.26+.

Commands

Command Alias Description
gogen new Create a new project
gogen generate migration gogen g migration Add a migration file
gogen generate auth gogen g auth Add auth to an existing project
gogen generate oauth gogen g oauth Add OAuth2 social login providers
gogen generate scaffold gogen g s Generate full CRUD for a model
gogen generate attribute gogen g a Add fields to an existing scaffold
gogen generate api gogen g api Add JSON API handler to an SSR scaffold
gogen generate controller gogen g controller Generate a simple page/API controller
gogen destroy scaffold gogen d s Remove a generated scaffold
gogen destroy controller gogen d controller Remove a generated controller

gogen new

Create a new project. Prompts for anything not supplied via flags.

gogen new <project-name> [flags]

Flags

Flag Short Description
--module -m Go module path (e.g. github.com/you/myapp)
--db -d Database: sqlite or postgres
--render -r Render mode: ssr, api, or both
--auth Include authentication
--no-auth Skip auth (skip the prompt)
--force / --dry-run / --skip File conflict behaviour

Examples

# Interactive — prompts for everything
gogen new myapp

# Fully non-interactive
gogen new myapp -m github.com/you/myapp -d sqlite -r ssr --auth
gogen new myapi -m github.com/you/myapi -d postgres -r api --no-auth

What gets generated

myapp/
├── main.go                              # bootstrap.Run()
├── go.mod                               # go 1.26
├── .env
├── .air.toml
├── Makefile
├── Dockerfile
├── docker-compose.yml
├── .gogen.yaml                          # project metadata for generate commands
├── bootstrap/
│   ├── app.go                           # Run() — DB init + server start
│   ├── config.go                        # env-based config (autoloads .env)
│   ├── server.go                        # graceful shutdown
│   ├── router.go                        # chi router + middleware (auto-updated)
│   └── wire_gen.go                      # Handlers struct + WireHandlers (auto-updated)
├── internal/
│   ├── domain/
│   │   ├── errors.go                    # ErrNotFound, ErrConflict, ErrUnauthorized, etc.
│   │   ├── session_port.go              # SessionStore, SessionService interfaces
│   │   ├── user.go                      # User, NewUser, Validate(), context helpers (with --auth)
│   │   ├── auth_port.go                 # UserStore, UserService interfaces (with --auth)
│   │   └── email_port.go                # EmailProvider interface (with --auth)
│   ├── application/
│   │   ├── auth_service.go              # login/signup/reset logic (with --auth)
│   │   └── session_service.go           # in-memory session cache (with --auth)
│   ├── utils/
│   │   ├── http_utils.go                # WriteJSON, DecodeJSON, cookies
│   │   └── validation.go                # HashPassword, PreHashing (with --auth)
│   └── adapters/
│       ├── api/
│       │   ├── middleware.go            # SecurityHeaders, LimitRequestBody, NoCache
│       │   ├── middleware_auth.go       # RequireAuth (with --auth)
│       │   ├── errors.go                # writeError — maps domain errors to HTTP status codes
│       │   └── auth_handler.go          # login/signup/reset routes (with --auth)
│       ├── web/                         # SSR only
│       │   └── renderer.go              # web.Render, web.RenderError — templ renderer
│       ├── db/
│       │   ├── store.go                 # DB connection + pool
│       │   ├── migrations.go            # goose embed runner
│       │   ├── auth_store.go            # user/session queries (with --auth)
│       │   └── migrations/
│       │       └── 00001_init.sql
│       └── external/email/
│           └── noop.go                  # email provider stub (with --auth)
└── web/                                 # SSR only
    ├── static.go
    ├── static/robots.txt
    └── components/
        ├── components.templ             # shared components (nav, etc.)
        ├── landing.templ
        ├── dashboard.templ              # (with --auth)
        └── auth/                        # (with --auth)
            ├── login.templ
            ├── signup.templ
            ├── forgot_password.templ
            ├── reset_password.templ
            └── settings.templ

Stack

Concern Library
Router chi
SQLite modernc.org/sqlite (pure Go, no CGO)
Postgres pgx/v5
Migrations goose v3 (embedded SQL)
Templates templ — type-safe Go SSR components
Env vars godotenv/autoload — loads .env at startup
Password bcrypt with sha256 pre-hashing

Docker

Standard 2-stage build using golang:1.26-alpine:

Stage 1 — builder    go build (CGO_ENABLED=0)
Stage 2 — runtime    alpine:3.21 + ca-certificates + tzdata

Both SQLite (modernc.org/sqlite) and Postgres (pgx/v5) are pure Go — no CGO needed.


gogen generate migration

Create a numbered migration file in internal/adapters/db/migrations/. Reads .gogen.yaml for the DB dialect.

gogen g migration <name> [field:type ...]

Smart name parsing

When the migration name follows a Rails-style convention and field args are provided, gogen generates the SQL automatically.

Pattern Args Generated SQL
AddXxxToTable field:type ... ALTER TABLE table ADD COLUMN ...
RemoveXxxFromTable field:type ... ALTER TABLE table DROP COLUMN ...
RenameOldToNewInTable (none) ALTER TABLE table RENAME COLUMN old TO new

Examples

# Empty skeleton
gogen g migration add_avatar_to_users
# → 00002_add_avatar_to_users.sql  (empty Up/Down)

# Add columns
gogen g migration AddStatusToOrders status:string
# → ALTER TABLE orders ADD COLUMN status TEXT NOT NULL DEFAULT '';

# Remove columns
gogen g migration RemoveStatusFromOrders status:string
# → ALTER TABLE orders DROP COLUMN status;

# Rename a column
gogen g migration RenamePartNumberToSkuInProducts
# → ALTER TABLE products RENAME COLUMN part_number TO sku;

Down migrations are generated automatically (reverse of Up). Reference fields also create/drop the index in the Down block.


gogen generate auth

Add authentication to a project that was created without it.

gogen g auth

Must be run from inside a gogen project with auth: false in .gogen.yaml.

What it does

  • Creates all auth files (domain, application, utils, handler, store, email stub)
  • Regenerates main.go, bootstrap/router.go, and bootstrap/wire_gen.go to wire auth in
  • Re-wires all existing scaffolds in wire_gen.go and router.go
  • Creates a new migration (NNNNN_add_auth.sql) with the auth tables
  • Adds SSR auth templ components if the project uses SSR
  • Adds User to .gogen.yaml scaffolds so gogen g attribute User works
  • Updates .gogen.yaml to auth: true

Auth tables created

  • users — email, password_hash, full_name, avatar_url, timezone, soft delete
  • sessions — token-based, 30-day expiry
  • password_reset_tokens — single-use, expiring
  • password_reset_attempts — rate limiting (3 per hour per email)

Auth routes

Method Path Description
GET /auth/login Login page (SSR)
POST /auth/login Authenticate
GET /auth/signup Signup page (SSR)
POST /auth/signup Register
POST /auth/logout Clear session
POST /auth/forgot-password Request reset link
POST /auth/reset-password Reset with token
GET /auth/settings Settings page (authenticated)
POST /auth/settings/password Change password (authenticated)
POST /auth/settings/delete Delete account (authenticated)

Adding fields to the User model

Use gogen g attribute User to add columns to the users table. Regenerates internal/domain/user.go and creates the migration. The auth store, service, and handler are left untouched.

gogen g attribute User role:string plan:string
# → NNNNN_add_role_plan_to_users.sql
# → internal/domain/user.go updated with Role, Plan fields

After running, update internal/adapters/db/auth_store.go to SELECT/INSERT the new columns.


gogen generate oauth

Add OAuth2 social login to a project that already has auth enabled. Supports Google, Apple, and Microsoft.

gogen g oauth <provider...>

Requires auth: true in .gogen.yaml. Can be run multiple times to add more providers.

Supported providers

Provider Env vars required
google GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET
microsoft MICROSOFT_CLIENT_ID, MICROSOFT_CLIENT_SECRET
apple APPLE_CLIENT_ID, APPLE_TEAM_ID, APPLE_KEY_ID, APPLE_PRIVATE_KEY

Also requires APP_URL (e.g. https://yourapp.com) for callback URLs.

Examples

gogen g oauth google
gogen g oauth google apple microsoft

What it generates (first run)

internal/adapters/db/migrations/NNNNN_add_oauth_to_users.sql   # adds provider + provider_id columns
internal/domain/oauth_port.go                                   # OAuthStore interface
internal/adapters/db/oauth_store.go                             # UpsertOAuthUser implementation
internal/adapters/api/oauth_handler.go                          # OAuth routes

On subsequent runs (adding more providers), only oauth_handler.go is regenerated to register the new provider.

OAuth routes

Method Path Description
GET /auth/oauth/{provider} Redirect to provider
GET /auth/oauth/{provider}/callback Handle callback, create session

Account linking

On callback, UpsertOAuthUser resolves the user in order:

  1. Find by (provider, provider_id) — returning user
  2. Find by email — links the OAuth identity to the existing account
  3. Neither — creates a new user

Install deps in your project

go get github.com/markbates/goth github.com/gorilla/sessions

.gogen.yaml tracking

oauth:
  providers: [google, apple]

gogen generate scaffold

Generate a full CRUD resource: migration, domain, port, store, service, and HTTP handler.

gogen g scaffold <ModelName> [field:type ...] [--protected]

Must be run from inside a gogen project (reads .gogen.yaml). Auto-updates bootstrap/router.go and bootstrap/wire_gen.go.

Field types

Type Go type SQLite Postgres
string / text string TEXT NOT NULL DEFAULT '' TEXT NOT NULL DEFAULT ''
int int INTEGER NOT NULL DEFAULT 0 INTEGER NOT NULL DEFAULT 0
bool bool INTEGER NOT NULL DEFAULT 0 BOOLEAN NOT NULL DEFAULT false
float float64 REAL NOT NULL DEFAULT 0 NUMERIC NOT NULL DEFAULT 0
time time.Time DATETIME TIMESTAMPTZ
uuid string TEXT NOT NULL DEFAULT '' UUID NOT NULL DEFAULT gen_random_uuid()
references string TEXT NOT NULL REFERENCES {table}(id) ON DELETE CASCADE UUID NOT NULL REFERENCES {table}(id) ON DELETE CASCADE
user:references string TEXT REFERENCES users(id) ON DELETE SET NULL UUID REFERENCES users(id) ON DELETE SET NULL

references is convention-based: post:referencespost_id column → FK to posts(id). Table name is auto-pluralized (categorycategories).

user:references (the literal form, column user_id) is generated as nullable. On non-protected routes, anonymous callers produce a record with user_id = NULL; authenticated callers get their ID injected automatically. On --protected routes, RequireAuth ensures a user is always present so NULL never occurs in practice.

When you need two FK columns pointing to the same table, use the aliased form alias:model:references:

# Two FKs to users: user_id and manager_id both reference users(id)
gogen g scaffold Employee user:references manager:user:references

# Two FKs to words: word_id and translate_id both reference words(id)
gogen g scaffold WordAssociation word:references translate:word:references
  • manager:user:references → column manager_id, FK to users(id), route /by-manager/{managerID}
  • translate:word:references → column translate_id, FK to words(id), route /by-translate/{translateID}

Aliased refs to users are treated as regular (non-auth-scoped) refs. Only the literal user:references (column user_id) triggers auth scoping with --protected.

Go field names follow standard acronym rules: user_idUserID, avatar_urlAvatarURL.

Example

gogen g scaffold Post title:string body:text user:references published:bool

Generated files

internal/domain/post.go
internal/domain/post_port.go
internal/application/post_service.go
internal/adapters/db/post_store.go
internal/adapters/api/post_handler.go
internal/adapters/db/migrations/NNNNN_create_posts.sql

With --render ssr or --render both, a templ component folder is also created:

web/components/posts/
├── index.templ
├── show.templ
├── new.templ
└── edit.templ

With --render both, an SSR web handler and API handler are both generated:

internal/adapters/web/post_handler.go      # SSR handler (GET /posts → HTML)
internal/adapters/api/post_handler.go      # API handler (GET /api/posts → JSON)

Validation

Generated domain structs include a Validate() method that checks required string/reference fields. The service layer calls it automatically on create and update — no validation logic leaks into handlers or utils.

// internal/domain/post.go
func (m Post) Validate() error {
    if strings.TrimSpace(m.Title) == "" {
        return fmt.Errorf("%w: title is required", ErrInvalidInput)
    }
    return nil
}

HTTP endpoints

Method Path Handler
GET /posts list all
POST /posts create
GET /posts/{id} get one
PUT / POST /posts/{id} update (PUT for API, POST for SSR forms)
DELETE / POST /posts/{id}/delete delete (DELETE for API, POST for SSR forms)

Association queries

When a references field is present, gogen generates filtered query methods at every layer.

For non-user refs (e.g. post:references), a list-by route is also exposed:

Layer Method
Store ListCommentsByPostID(ctx, postID)
Service ListByPostID(ctx, postID)
Handler GET /comments/by-post/{postID}

user:references is treated specially — no public list-by route is generated. Instead, --protected + user:references scopes the default list endpoint to the current user automatically.

Multiple refs each get their own method and route:

gogen g scaffold Comment body:text post:references category:references
# GET /comments/by-post/{postID}
# GET /comments/by-category/{categoryID}

--protected flag

Requires auth: true in .gogen.yaml. Mounts the scaffold routes inside the RequireAuth middleware group.

gogen g scaffold Post title:string body:text user:references --protected

Three things happen automatically:

  1. Routes are mounted inside the protected group (behind RequireAuth)
  2. create injects the current user's ID into the user_id field (when user:references is present)
  3. list uses service.ListByUserID(ctx, userID) instead of service.List(ctx) — scoped to current user

Auto-wiring

After generation, bootstrap/wire_gen.go and bootstrap/router.go are updated automatically — no manual edits needed:

// bootstrap/wire_gen.go (auto-generated)
type Handlers struct {
    Post *api.PostHandler
}

func WireHandlers(dbStore *db.Store, logger *slog.Logger) *Handlers {
    h := &Handlers{}
    postSvc := application.NewPostService(dbStore)
    h.Post = api.NewPostHandler(postSvc, logger)
    return h
}

gogen generate attribute

Add new fields to an existing scaffold. Updates the domain, store, and handler; creates an ALTER TABLE migration; and regenerates SSR templ components if applicable.

gogen g attribute <ModelName> field:type [field:type ...]

Must be run from inside a gogen project. The model must already exist (created via gogen g scaffold or gogen g auth).

Example

gogen g attribute Post published:bool views:int

What it does:

  • Creates NNNNN_add_published_views_to_posts.sql with ALTER TABLE statements
  • Regenerates post.go, post_store.go, post_handler.go with the new fields
  • Regenerates SSR templ components (web/components/posts/*.templ) only when --views is passed
  • Updates .gogen.yaml with the new field list

Accepts the same field types as gogen g scaffold. Duplicate fields are rejected.

SSR views are not regenerated by default to preserve any customisations you've made. Pass --views to overwrite them.

Auth User model

gogen g attribute User is supported after gogen g auth. It regenerates internal/domain/user.go using the auth-aware template (preserving all auth logic) and creates the migration. The auth store, service, and handler are not touched — update auth_store.go manually to SELECT/INSERT the new columns.

gogen g attribute User role:string
# → NNNNN_add_role_to_users.sql
# → internal/domain/user.go  (Role string field added to User struct)
# → hint: update auth_store.go SELECT/INSERT

gogen generate api

Add a JSON API handler to an existing SSR scaffold. Useful when you want to expose a REST API alongside your server-rendered pages.

gogen g api <ModelName>

Must be run from inside a gogen project with render: ssr. The scaffold must already exist.

Example

gogen g api Post
# generates: internal/adapters/api/post_api_handler.go
# routes:    GET /api/posts, POST /api/posts, GET /api/posts/{id}, etc.

Updates .gogen.yaml and rewires bootstrap/wire_gen.go and bootstrap/router.go automatically.


gogen generate controller

Generate a simple page or API controller with no model, store, or service — useful for static-ish pages like contact, about, terms, etc.

gogen g controller <Name> [--protected] [--route /path]

Flags

Flag Description
--protected Mount behind RequireAuth middleware
--route Custom route path (default: /<name>)

Examples

gogen g controller Contact
# GET /contact → web/components/contact/page.templ

gogen g controller Dashboard --protected --route /dashboard
# GET /dashboard → protected, web/components/dashboard/page.templ

Generated files (SSR)

internal/adapters/web/contact_handler.go
web/components/contact/page.templ

Generated files (API)

internal/adapters/api/contact_handler.go

Auto-wired into bootstrap/wire_gen.go and bootstrap/router.go.


gogen destroy scaffold

Remove all files generated by gogen g scaffold. Updates bootstrap/wire_gen.go and bootstrap/router.go automatically.

gogen d scaffold <ModelName>

Example

gogen d scaffold Post

Removes:

internal/domain/post.go
internal/domain/post_port.go
internal/application/post_service.go
internal/adapters/db/post_store.go
internal/adapters/api/post_handler.go
internal/adapters/api/post_api_handler.go   # both/api mode
internal/adapters/web/post_handler.go       # ssr/both mode
web/components/posts/                        # SSR only
internal/adapters/db/migrations/*_create_posts.sql

Migration warning

If a matching migration file is found it is deleted, but a warning is printed:

warning migration 00003_create_posts.sql was deleted — run goose down manually if already applied

If you already ran goose up against a real database, run the down migration first:

goose -dir internal/adapters/db/migrations sqlite3 myapp.db down
gogen d scaffold Post

--dry-run prints what would be removed without deleting anything.


gogen destroy controller

Remove all files generated by gogen g controller. Updates bootstrap/wire_gen.go and bootstrap/router.go automatically.

gogen d controller <Name>

Example

gogen d controller Contact

Removes:

internal/adapters/web/contact_handler.go   # SSR
internal/adapters/api/contact_handler.go   # API
web/components/contact/                    # SSR

Templates (SSR)

Generated projects use templ — a type-safe Go templating language that compiles to plain Go functions.

Define a component (web/components/card/card.templ):

package card

templ Card(title, body string) {
    <div class="card">
        <h2>{ title }</h2>
        <p>{ body }</p>
    </div>
}

Use it in a page (web/components/posts/index.templ):

package posts

import "myapp/web/layouts"
import "myapp/web/components/card"

templ Index(posts []domain.Post) {
    @layouts.Layout("Posts") {
        for _, p := range posts {
            @card.Card(p.Title, p.Body)
        }
    }
}

Render from a handler:

web.Render(w, r, posts.Index(items))

Error rendering:

web.RenderError(w, r, err)   // maps domain errors to HTTP status + error page

web.RenderError maps domain errors to the correct HTTP status automatically:

Domain error HTTP status
ErrNotFound 404
ErrUnauthorized 401
ErrForbidden 403
ErrConflict 409
anything else 500

Error handling

Domain errors are defined in internal/domain/errors.go and mapped to HTTP status codes only at the adapter layer — never inside domain or application code.

API adapter (internal/adapters/api/errors.go):

writeError(w, err)   // maps domain errors to JSON error responses

SSR adapter (internal/adapters/web/renderer.go):

web.RenderError(w, r, err)   // maps domain errors to HTML error pages

Global flags

Available on all commands:

Flag Short Description
--force -f Overwrite existing files
--dry-run -p Preview without writing
--skip -s Skip existing files silently

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors