Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,22 @@ mobile/
CLAUDE.md → AGENTS.md # mobile-specific rules (read before writing Kotlin/Compose)
```

## Swagger / OpenAPI

Swagger UI is served at `http://localhost:8080/swagger/index.html`. Docs are generated from swaggo annotations and committed to `backend/docs/swagger/`.

**Every new route must include swaggo annotations.** Run `make swagger` from `backend/` after adding or changing any handler to regenerate the docs package and commit the updated files.

Annotation rules:
- API-level metadata (`@title`, `@version`, `@host`, `@BasePath`) lives in `cmd/api/main.go` above `main()`.
- Handler-level annotations go immediately above each handler method: `@Summary`, `@Tags`, `@Produce`, `@Param` (if applicable), `@Success`, `@Failure`, `@Router`.
- If a handler returns a `domain.*` type, add a `type Alias = domain.Type` line to `internal/transport/handlers/swagger_types.go` and reference it as `{object} Alias` in the annotation — swaggo cannot traverse the `usecase → domain` import chain automatically.
- See `backend/docs/swagger.md` for the full annotation reference and examples.

```bash
cd backend && make swagger # regenerate docs/swagger/ after annotation changes
```

## Go conventions (Clean Architecture)
- Follow the dependency rule: `domain` ← `usecase` ← `transport`/`infrastructure` ← `server` ← `cmd`.
- New feature → add entity to `domain/`, interface to `usecase/`, implementation to `infrastructure/database/postgres/`, handler to `transport/handlers/`, wire in `server/server.go`.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ Copy `backend/.env` and fill in your values. Never commit secrets to source cont
| Variable | Description | Default |
|-------------------------|------------------------------|-------------|
| `PORT` | Backend server port | `8080` |
| `APP_ENV` | Environment (`local`/`prod`) | `local` |
| `ENV` | Environment (`local`/`staging`/`production`) | `local` |
| `BLUEPRINT_DB_HOST` | Postgres host | `localhost` |
| `BLUEPRINT_DB_PORT` | Postgres port | `5432` |
| `BLUEPRINT_DB_DATABASE` | Database name | `blueprint` |
Expand Down
2 changes: 1 addition & 1 deletion backend/.env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PORT=8080
APP_ENV=local
ENV=local #local/staging/production
BLUEPRINT_DB_HOST=localhost
BLUEPRINT_DB_PORT=5432
BLUEPRINT_DB_DATABASE=blueprint
Expand Down
7 changes: 6 additions & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ clean:
@echo "Cleaning..."
@rm -f main

# Generate Swagger docs (requires annotations to be up to date)
swagger:
@echo "Generating Swagger docs..."
@go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g cmd/api/main.go -o docs/swagger

# Live Reload
watch:
@powershell -ExecutionPolicy Bypass -Command "if (Get-Command air -ErrorAction SilentlyContinue) { \
Expand Down Expand Up @@ -72,6 +77,6 @@ migrate-version:
migrate-create:
@go run ./cmd/migrate create $(name)

.PHONY: all build run test clean watch docker-run docker-down itest \
.PHONY: all build run test clean watch docker-run docker-down itest swagger \
migrate-up migrate-up-one migrate-down migrate-down-to migrate-reset \
migrate-status migrate-version migrate-create
7 changes: 7 additions & 0 deletions backend/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ func gracefulShutdown(apiServer *http.Server, done chan bool) {
done <- true
}

// @title Blueprint API
// @version 1.0
// @description Fullstack template REST API.
//
// @host localhost:8080
// @BasePath /
func main() {
// Signal-aware context so SIGINT/SIGTERM cancels bootstrap probes immediately.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
Expand All @@ -45,6 +51,7 @@ func main() {
}

srv := server.NewServer(app)
slog.Info("API docs", "url", fmt.Sprintf("http://localhost%s/swagger/index.html", srv.Addr))

done := make(chan bool, 1)
go gracefulShutdown(srv, done)
Expand Down
116 changes: 116 additions & 0 deletions backend/docs/swagger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
topic: swagger
last_verified: 2026-06-15
sources:
- cmd/api/main.go
- internal/transport/handlers/routes.go
- internal/transport/handlers/swagger_types.go
- docs/swagger/docs.go
- Makefile
---

# Swagger / OpenAPI Documentation

## Overview

Swagger UI is served at `GET /swagger/index.html` (proxied via `/swagger/*any`). The generated spec files live in `docs/swagger/` and are committed to version control.

**Tool:** [swaggo/swag](https://github.com/swaggo/swag) v1.16.x
**UI middleware:** `github.com/swaggo/gin-swagger` + `github.com/swaggo/files`

## Regenerating docs

After adding or changing any handler annotation, run from `backend/`:

```bash
make swagger
```

This runs:
```bash
go run github.com/swaggo/swag/cmd/swag@v1.16.6 init -g cmd/api/main.go -o docs/swagger
```

Commit the updated `docs/swagger/docs.go`, `docs/swagger/swagger.json`, and `docs/swagger/swagger.yaml` alongside the handler change.

## Annotation locations

### API-level metadata — `cmd/api/main.go`

Place the block immediately above `func main()`:

```go
// @title Blueprint API
// @version 1.0
// @description Fullstack template REST API.
//
// @host localhost:8080
// @BasePath /
func main() {
```

### Handler-level annotations

Place the block immediately above the handler method:

```go
// @Summary Short description
// @Tags tagname
// @Produce json
// @Param id path int true "Record ID"
// @Success 200 {object} ResponseType
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /resource/{id} [get]
func (h *Handler) MyHandler(c *gin.Context) {
```

## Referencing domain types

Swaggo resolves types from the handler file's own imports. The `handlers` package does **not** directly import `backend/internal/domain`, so you cannot write `{object} domain.HealthStats` in an annotation.

**Fix:** add a type alias to `internal/transport/handlers/swagger_types.go`:

```go
// swagger_types.go
package handlers

import "backend/internal/domain"

type HealthStats = domain.HealthStats
// Add more aliases here as new domain types are introduced
```

Then reference the alias in the annotation:

```go
// @Success 200 {object} HealthStats
```

## Common `@Param` sources

| Source | swag keyword |
|--------|-------------|
| URL path | `path` |
| Query string | `query` |
| Request body | `body` |
| Header | `header` |

Example with a body param:

```go
// @Param payload body CreateUserRequest true "User payload"
```

Where `CreateUserRequest` is a struct defined in the `handlers` package (or aliased in `swagger_types.go`).

## Tags

Group related endpoints with the same `@Tags` value. Current tags:

| Tag | Endpoints |
|-----|-----------|
| `general` | `GET /` |
| `ops` | `GET /health` |

Add new tags as needed — swag collects them automatically.
121 changes: 121 additions & 0 deletions backend/docs/swagger/docs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
package swagger

import "github.com/swaggo/swag"

const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/": {
"get": {
"produces": [
"application/json"
],
"tags": [
"general"
],
"summary": "Hello World",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/health": {
"get": {
"produces": [
"application/json"
],
"tags": [
"ops"
],
"summary": "Health check",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.HealthStats"
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"$ref": "#/definitions/handlers.HealthStats"
}
}
}
}
}
},
"definitions": {
"handlers.HealthStats": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"idle": {
"type": "integer"
},
"in_use": {
"type": "integer"
},
"max_idle_closed": {
"type": "integer"
},
"max_lifetime_closed": {
"type": "integer"
},
"message": {
"type": "string"
},
"open_connections": {
"type": "integer"
},
"status": {
"type": "string"
},
"wait_count": {
"type": "integer"
},
"wait_duration": {
"type": "string"
}
}
}
}
}`

// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "1.0",
Host: "localhost:8080",
BasePath: "/",
Schemes: []string{},
Title: "Blueprint API",
Description: "Fullstack template REST API.",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
LeftDelim: "{{",
RightDelim: "}}",
}

func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}
Loading
Loading