Go API AI Rules

Rules for Go HTTP services: standard-library-first, chi for routing, sqlc for type-safe queries, structured errors, context propagation, and testable handlers.

Gochi / standard library#go#golang#chi#sqlcLast updated 2026-05-05
tune

Want to customize this rules file? Open the generator with this stack pre-loaded.

Open in generatorarrow_forward

Save at .cursor/rules/main.mdc

Go API (chi + sqlc)

Project context

This is a Go HTTP service. We follow the Go community's "standard library first" instinct: chi for routing because it's idiomatic and tiny, sqlc for type-safe queries against Postgres, and as little framework as possible. Concurrency is structured; errors are values; context propagates through every call.

Stack

  • Go 1.22+
  • chi v5 for HTTP routing
  • sqlc for SQL → Go type generation
  • Postgres 15+ via pgx/v5
  • slog (stdlib structured logging)
  • testify or stdlib testing
  • golangci-lint
  • migrate for DB migrations

Folder structure

.
├── cmd/
│   └── api/
│       └── main.go         — entrypoint
├── internal/
│   ├── config/             — env config
│   ├── http/
│   │   ├── handlers/       — HTTP handlers
│   │   ├── middleware/
│   │   └── router.go
│   ├── service/            — business logic
│   ├── repo/               — data access (wraps sqlc)
│   ├── domain/             — pure types and interfaces
│   └── platform/           — DB, cache, queue clients
├── db/
│   ├── migrations/
│   ├── queries/            — .sql files for sqlc
│   └── sqlc/               — generated code (committed)
├── go.mod
├── go.sum
└── sqlc.yaml

internal/ keeps these packages from being imported by external Go code. cmd/<name> per binary.

Idioms

  • Error wrapping: fmt.Errorf("doing X: %w", err) always; never fmt.Errorf("doing X: %s", err)
  • errors.Is / errors.As to inspect wrapped errors; never string compare
  • Sentinel errors in the package that defines the operation:
    var ErrNotFound = errors.New("not found")
    
  • Context first: func (s *Service) GetPost(ctx context.Context, id string) (*Post, error)
  • Accept interfaces, return structs. Define the interface in the consumer; the producer returns its concrete type.
  • Zero-value usable when sensible. A struct with no init function reduces ceremony.

sqlc workflow

  1. Write the CREATE TABLE migration in db/migrations/
  2. Write the SQL query in db/queries/<resource>.sql with -- name: GetPost :one annotations
  3. Run sqlc generate — produces type-safe Go in db/sqlc/
  4. Wrap generated code in a repo type that takes context.Context and returns domain types
-- name: GetPostByID :one
SELECT id, title, body, created_at FROM posts WHERE id = $1;

-- name: ListPosts :many
SELECT id, title, body, created_at FROM posts ORDER BY created_at DESC LIMIT $1;
type PostRepo struct{ q *sqlc.Queries }

func (r *PostRepo) GetByID(ctx context.Context, id string) (*domain.Post, error) {
    row, err := r.q.GetPostByID(ctx, id)
    if err != nil {
        if errors.Is(err, pgx.ErrNoRows) { return nil, ErrNotFound }
        return nil, fmt.Errorf("get post: %w", err)
    }
    return toDomain(row), nil
}

HTTP handlers

func (h *Handler) ListPosts(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    posts, err := h.posts.List(ctx, 20)
    if err != nil {
        h.error(w, r, err)
        return
    }
    writeJSON(w, http.StatusOK, posts)
}
  • Always thread r.Context() into service / repo calls
  • Use one error(w, r, err) helper that maps domain errors → HTTP status
  • Helpers like decodeJSON(r, &v) and writeJSON(w, status, v) keep handlers small

Concurrency

  • Use errgroup.Group for "fan out, wait, propagate first error"
  • Use sync.WaitGroup only when you genuinely don't want errors to short-circuit
  • Always derive a child context with context.WithTimeout for outbound calls
  • Never spawn a goroutine from a handler without a way to wait for or cancel it

Configuration

  • Read env once in internal/config, return a typed struct
  • Validate at startup; fail fast on missing or malformed values
  • Never os.Getenv inside business logic

Logging

  • slog with a structured handler; one logger at the root, child loggers per request
  • Include request ID via middleware; pass through context
  • Don't log secrets, tokens, or PII

Migrations

  • migrate create -ext sql -dir db/migrations -seq <name>
  • Always include up.sql and down.sql
  • Never modify a committed migration; create a new one
  • Run in CI/CD: migrate -path db/migrations -database $DATABASE_URL up

Patterns to avoid

  • Naked returns in long functions
  • panic for normal errors — return them
  • init() for non-trivial setup — call from main
  • Global state — pass dependencies explicitly
  • Empty interface (any / interface{}) as a parameter type — be specific
  • Long functions — extract once they grow past ~50 lines, but don't over-extract trivial logic

Testing

  • Stdlib testing; testify only for assertions when stdlib is verbose
  • Table-driven tests for any function with multiple inputs:
    for _, tc := range []struct{ name string; in, want X }{ ... } {
        t.Run(tc.name, func(t *testing.T) { ... })
    }
    
  • Use httptest.NewRecorder for handler tests
  • For DB tests, use a real Postgres in Docker; truncate between tests

Tooling

  • go run ./cmd/api
  • go build ./...
  • go test ./... -race
  • golangci-lint run
  • sqlc generate after editing queries
  • migrate up / migrate down

AI behavioral rules

  • Always wrap errors with %w; never %s
  • Always thread context.Context as the first parameter
  • Use sentinel errors + errors.Is for typed checks; never string-compare
  • After editing a .sql query file, run sqlc generate — don't hand-edit generated code
  • Never modify a committed migration; create a new one
  • Don't introduce a framework when the standard library suffices
  • Run go test ./... -race and golangci-lint run before declaring a task done

Frequently asked

How do I use this chi / standard library rules file with Cursor?

Pick "Cursor (.cursor/rules/*.mdc)" from the format dropdown above and click Copy. Save it at .cursor/rules/main.mdc in your project root and restart Cursor. The legacy .cursorrules format still works if you're on an older Cursor version — pick that option instead.

Can I use this with Claude Code (CLAUDE.md)?

Yes — pick "Claude Code (CLAUDE.md)" from the format dropdown above and copy. Save the file as CLAUDE.md at your repo root. Claude Code reads it automatically on every session. For monorepos, you can also drop nested CLAUDE.md files in subdirectories — Claude merges them when working in those paths.

Where exactly do I put this file?

It depends on the AI tool. Cursor reads .cursorrules or .cursor/rules/*.mdc at the project root. Claude reads CLAUDE.md at the project root. Copilot reads .github/copilot-instructions.md. The "Save at" path under each format in the dropdown shows the exact location for the format you picked.

Can I customize these chi / standard library rules for my project?

Yes — that's what the generator is for. Click "Open in generator" above and the wizard loads with this stack's defaults pre-selected. Toggle on or off the conventions you want, then re-export in your AI tool's format.

Will using this rules file slow down my AI tool?

No. Rules files count toward the model's context window but not toward latency in any noticeable way. The file is loaded once per session, not per token. The library files target 250–400 lines, well within every tool's recommended budget.

Should I commit this file to git?

Yes. The rules file is project documentation that benefits every developer using the AI tool. Commit it. The exception is personal-global settings (e.g. ~/.claude/CLAUDE.md) which are user-scoped and stay out of the repo.