Imported from Piszmog/go-htmx-template (
AGENTS.md). Install upstream withnpx skills add Piszmog/go-htmx-template. Copyright stays with the author.
AGENTS.md - Development Guidelines
This file contains comprehensive guidelines for AI coding agents working in this Go + HTMX template repository.
Build/Test/Lint Commands
Development
- Run with live reload:
air(auto-generates templ, sqlc, tailwind CSS on file changes) - Manual build:
go build -o ./tmp/main ./cmd/server - Run without air:
go run ./cmd/server
Testing
- All tests:
go test -v ./... - With race detector:
go test -race ./... - Single package:
go test -v ./internal/server/handler - Single test:
go test -v ./internal/server/handler -run TestHome - E2E tests only:
go test -v ./... -tags=e2e - E2E single test:
go test -v ./e2e -tags=e2e -run TestHomePage - Headful E2E (see browser):
HEADFUL=1 go test -v ./e2e -tags=e2e - Different browser:
BROWSER=firefox go test -v ./e2e -tags=e2e(chromium, firefox, webkit)
Security Testing
- All security tests:
go test -v ./e2e -tags=e2e -run "TestSecurity|TestCSRF|TestRate|TestServer" - Security headers:
go test -v ./e2e -tags=e2e -run TestSecurityHeaders - CSRF protection:
go test -v ./e2e -tags=e2e -run TestCSRFProtection - Rate limiting:
go test -v ./e2e -tags=e2e -run TestRateLimiting - Server timeouts:
go test -v ./e2e -tags=e2e -run TestServerTimeouts - Vulnerability scan:
govulncheck ./...(install:go install golang.org/x/vuln/cmd/govulncheck@latest)
Linting
- Lint all:
golangci-lint run - Lint with fixes:
golangci-lint run --fix - SQL lint:
go tool sqlc vet
Code Generation
- Generate all:
go tool templ generate -path ./internal/components && go tool sqlc generate - Templ only:
go tool templ generate -path ./internal/components - SQLC only:
go tool sqlc generate - Tailwind CSS:
go tool go-tw -i ./styles/input.css -o ./internal/dist/assets/css/output@dev.css
Database Migrations
- Create migration:
go run github.com/golang-migrate/migrate/v4/cmd/migrate@v4.19.1 create -ext sql -dir internal/db/migrations <name> - Migration files: Creates two files:
<timestamp>_<name>.up.sqland<timestamp>_<name>.down.sql - Up migrations: Write schema changes in
.up.sql(e.g.,CREATE TABLE,ALTER TABLE) - Down migrations: Write rollback logic in
.down.sql(e.g.,DROP TABLE) - Auto-run:
airruns./migrate.shautomatically on each rebuild (see.air.tomlpre_cmd). The server itself does not migrate on startup. - Manual run:
./migrate.sh -p sqlite -u ./db.sqlite3(up) or add-d down -s 1for rollback - Migration naming: Use descriptive names (e.g.,
add_users_table,add_email_to_authors)
Dependencies
- Update all:
go get -u ./... - Tidy:
go mod tidy - Update tools:
go get -u tool
Code Style & Conventions
Imports
- Order: Standard library first, blank line, third-party packages, blank line, local packages
- Example:
import ( "context" "fmt" "net/http" "github.com/a-h/templ" "go-htmx-template/internal/db" "go-htmx-template/internal/log" )
Naming
- Exported: PascalCase (e.g.,
Handler,Database,NewLogger) - Unexported: camelCase (e.g.,
defaultHandler,getPort) - Acronyms: Use uppercase for exported (e.g.,
HTML,DB,URL), lowercase for unexported (e.g.,db,url) - Interfaces: Name after what they do (e.g.,
Database) or add-ersuffix (e.g.,Handler)
Error Handling
- Always check errors: Never ignore error return values
- Wrap errors: Use
fmt.Errorfwith%wfor error context:fmt.Errorf("failed to query: %w", err) - Join errors: Use
errors.Join(err1, err2)for multiple errors (seeinternal/db/db.go:51) - Log then return: Log errors with context before returning:
h.Logger.Error("msg", "error", err)
Logging
- Use structured logging:
slog.Loggerwith key-value pairs - Example:
logger.Info("server started", "port", port, "env", env) - Error logs:
logger.Error("operation failed", "error", err, "context", value) - Inject logger: Pass
*slog.Loggerto structs via dependency injection (seeinternal/server/handler/handler.go:13)
Comments
- Document exports: All exported functions, types, methods need doc comments
- Format: Start with the name:
// Handler handles requests. - Single line: Use
//for all comments (avoid/* */except for package docs)
Interface Implementation
- Compile-time checks: Always add compile-time interface checks for structs that implement interfaces
- Format:
var _ InterfaceName = (*StructName)(nil)orvar _ InterfaceName = StructName{}(for value receivers) - Placement: Place the check immediately after the struct definition, before constructor functions
- Example:
type responseWriter struct { http.ResponseWriter statusCode int } // Compile-time check to ensure responseWriter implements http.ResponseWriter. var _ http.ResponseWriter = (*responseWriter)(nil) func newResponseWriter(w http.ResponseWriter) *responseWriter { // ... } - Purpose: Catches interface compliance issues at build time rather than runtime
Templ Syntax
- Components:
templ ComponentName(params) { <html>content</html> } - Expressions: Use
{ variable }for interpolation,{ function() }for function calls - Composition: Call other components with
@ComponentName(args) - All tags must be closed: Use
<div></div>or<br/>(self-closing) - Parameters: Accept Go types as parameters:
templ Button(text string, disabled bool) - File structure: Package declaration, imports, then templ components
- Generated files:
*.gofiles are auto-generated, edit only*.templfiles
HTMX Patterns
- Basic requests:
hx-get="/path",hx-post="/path",hx-put="/path",hx-delete="/path" - Triggers:
hx-trigger="click"(default),hx-trigger="change",hx-trigger="keyup delay:500ms" - Targets:
hx-target="#result",hx-target="closest tr",hx-target="next .error" - Swapping:
hx-swap="innerHTML"(default),hx-swap="outerHTML",hx-swap="afterend" - Indicators: Add
class="htmx-indicator"to show/hide loading states - Forms: Include form values automatically, use
hx-includefor additional inputs - Boosting:
hx-boost="true"converts links/forms to AJAX requests
SQLC Usage
- Query annotations:
-- name: FunctionName :one|:many|:exec(required for all queries) - Return types:
:one(single row),:many(slice),:exec(error only),:execresult(sql.Result) - Parameters: Use
?for SQLite placeholders in queries - Generated code: Run
go tool sqlc generateto create Go functions from SQL - File structure: Queries in
internal/db/queries/, migrations ininternal/db/migrations/, generated code ininternal/db/queries/ - Usage pattern:
queries := db.New(sqlDB); result, err := queries.FunctionName(ctx, params)
Tailwind CSS
- Utility-first: Use small, single-purpose classes like
text-center,bg-blue-500,p-4 - Responsive: Prefix utilities with breakpoints:
sm:text-left,md:flex,lg:grid-cols-3 - States: Use state prefixes:
hover:bg-blue-700,focus:ring-2,disabled:opacity-50 - Spacing: Use consistent scale:
p-4(padding),m-2(margin),gap-6(gap) - Colors: Use semantic names:
bg-red-500,text-gray-700,border-blue-200 - Layout: Common patterns:
flex items-center justify-between,grid grid-cols-2 gap-4 - Typography: Size and weight:
text-xl font-bold,text-sm text-gray-600
Architecture & Patterns
- Server: Uses standard library
http.ServeMuxwith graceful shutdown (SIGINT handling) - Middleware: Chain pattern with logging, caching, and custom middleware support
- Handlers: Struct-based handlers with dependency injection (logger, database)
- Database: Interface-based design (
db.Database) for easy testing/mocking - Logging: Structured logging with
slog, configurable viaLOG_LEVEL/LOG_OUTPUTenv vars - Context: Always pass
context.Contextas first parameter to functions that need it - Error handling: Use
fmt.Errorfwith%wfor error wrapping, log errors with context - Security: Secure-by-default with native CSRF protection (Go 1.25+), rate limiting, and security headers
- CSRF: Uses
http.CrossOriginProtection(no tokens needed, transparent protection via Sec-Fetch-Site header) - Rate Limiting: Per-IP token bucket algorithm, 50 requests/minute default, in-memory storage with auto-cleanup
- Security Headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, HSTS
- Server Hardening: Timeout configurations (ReadHeaderTimeout, IdleTimeout, MaxHeaderBytes) prevent slowloris attacks
- CSRF: Uses
Environment Variables
- PORT: Server port (default: 8080)
- LOG_LEVEL: debug, info, warn, error (default: info)
- LOG_OUTPUT: text, json (default: text)
- DB_URL: Database file path (default: ./db.sqlite3)
Testing
- E2E: Playwright tests with
//go:build e2etag, run withgo test -tags=e2e - Test setup: Automatic app startup, database seeding, random port allocation
- Browser support: Chromium (default), Firefox, WebKit via
BROWSERenv var
Project Structure
cmd/server/: Application entrypoint with main.gointernal/: All implementation packages (prevents external imports)components/: templ files (*.go files auto-generated, ignored by git)db/: sqlc generated code and migrationsserver/: HTTP handlers, middleware, routingdist/: Embedded static assets (CSS auto-generated, ignored by git)log/: Structured logging utilitiesversion/: Build-time version info (set via ldflags)
e2e/: End-to-end tests using Playwright (external to app)styles/: CSS source files (input for Tailwind)
Why internal/ Package?
All application code lives in internal/ following Go's official server project layout:
- Prevents external packages from importing implementation details
- Signals this is a server application, not a reusable library
- Follows go.dev/doc/modules/layout "Server project" pattern
cmd/server/contains the application entrypoint- Only
e2e/(tests) andstyles/(build inputs) stay at root