internal/api/README.md
The API package exposes PhotoPrism’s HTTP endpoints via Gin handlers. Each file under internal/api contains the handlers, request/response DTOs, and Swagger annotations for a specific feature area. Handlers remain thin: they validate input, enforce security or ACL checks, and delegate domain work to services in internal/photoprism, internal/service, or other internal packages. Keep exported types aligned with the REST schema and avoid embedding business logic directly in handlers.
internal/server/routes.go by attaching them to the proper router group (for example, APIv1 := router.Group(conf.BaseUri("/api/v1"), Api(conf))).Api, AuthRequired, limiter.Auth, etc.) at the router level to keep handlers focused on request handling.conf.BaseUri() when constructing route prefixes so configuration overrides propagate consistently.header.ContentTypeJSON and ensure responses include proper cache headers (no-store for sensitive payloads).safe.Download, avatar.SafeDownload) when calling outward HTTP APIs to inherit timeout, size, and SSRF protections.count, offset, and limit following the defaults (100 max 1000). Validate offset >= 0 and clamp count to the allowed range.AuthRequired) and check roles via helpers in internal/auth/acl (acl.ParseRole, acl.ScopePermits, acl.ScopeAttrPermits).LimitRequestBodyBytes(...) with a route-appropriate cap before BindJSON(...) / ShouldBindJSON(...), detect IsRequestBodyTooLarge(err), and return 413 Request Entity Too Large via AbortRequestTooLarge(...).make check-api-request-limits (also included in make lint) after adding or refactoring API handlers in the root repo or private overlays.event.Log and redact sensitive values before logging.limiter.Auth, limiter.Login) and respond with limiter.AbortJSON to maintain consistent 429 JSON payloads.api.ClientIP and extract bearer tokens with header.BearerToken or the helper setters. Use constant-time comparison for tokens and secrets.http, https) and reject private or loopback addresses unless explicitly required.event.Audit* (AuditInfo, AuditWarn, AuditErr, AuditDebug) and always build the slice as Who → What → Outcome.
ClientIP(c) followed by the most specific actor context ("session %s", "client %s", "user %s").string(acl.ResourceCluster), "node", "%s"). Place extra context such as counts or error placeholders in separate segments before the outcome.status.Succeeded, status.Failed, status.Denied, or status.Error(err) when the sanitized error message should be the outcome; nothing comes after it.ClientIP, clean.Log, clean.LogQuote, clean.Error) instead of formatting values manually, and avoid inline = expressions.event.AuditInfo([]string{
ClientIP(c),
"session %s",
string(acl.ResourceCluster),
"node", "%s",
status.Deleted,
}, s.RefID, uuid)
event.AuditErr([]string{
clientIp,
"session %s",
string(acl.ResourceCluster),
"download theme",
status.Error(err),
}, refID)
specs/common/audit-logs.md for the full conventions and additional examples that agents should follow./api/v1/... paths, request/response schemas, and security definitions. Only annotate routes that are externally accessible.make fmt-go swag-fmt swag. This formats Go files, normalizes annotations, and updates internal/api/swagger.json. Do not edit the generated JSON manually.NewApiTest) to obtain a configured Gin router, config, and dependencies. This isolates filesystem paths and avoids polluting global state.PerformRequestJSON, PerformAuthenticatedRequest) to capture status codes, headers, and payloads. Assert headers using constants from pkg/http/header.config.NewTestConfig("api") or config.NewMinimalTestConfigWithDb("api", t.TempDir()) depending on fixture needs.httptest.Server) for remote calls and set AllowPrivate=true explicitly when the test server binds to loopback addresses.t.Run("CaseName", ...)) and use PascalCase names. Provide cleanup functions (t.Cleanup) to remove temporary files or databases created during tests.internal/api tests in parallel. These suites share fixture files, temporary assets, and database state, so parallel go test invocations can cause false failures and readonly/fixture-conflict errors.go test ./internal/api -run '<Package|HandlerName>' -count=1go test ./internal/api -run 'Cluster' -count=1go test ./internal/api -run 'Download|Archive' -count=1go test ./internal/commands -run 'Cluster' -count=1 with the matching API suite to ensure DTOs remain compatible.internal/api runs sequential. Do not launch multiple go test ./internal/api ... commands at the same time.make fmt-go swag-fmt swaggo build ./...go test ./internal/api -run '<Name>' -count=1go test ./internal/service/cluster/registry -count=1 alongside relevant API routes to confirm cluster DTOs stay aligned.photoprism show commands --json reflects any new API-driven flags or outputs when CLI exposure changes.