API Patterns
This guide covers the conventions and patterns used in the Go backend API.
Endpoint Design
Route Structure
API routes follow RESTful conventions with nested resources:
/api/me/files → List files for the authenticated user
/api/files/:file_id → Get a specific file by ID
The /me Shorthand
/me is a shorthand for the authenticated user. Instead of /api/users/:id/files, we use /api/me/files. The user identity is resolved from the JWT — there's no need to pass it as a path parameter.
When to use /me: For resources scoped to the authenticated user (their files, settings, preferences).
When to use /resources/:id: For resources accessed by ID regardless of ownership (shared files, public content).
Route Registration
Routes are registered in cmd/api/main.go using chi:
r.Route("/api", func(r chi.Router) {
r.Use(clerkAuth) // All /api routes require authentication
r.Route("/me", func(r chi.Router) {
r.Get("/files", fileHandler.ListFiles)
})
r.Route("/files", func(r chi.Router) {
r.Get("/{file_id}", fileHandler.GetFile)
})
})
Handler → Service → Repository
Every API feature follows a three-layer architecture:
Handler (HTTP) → Service (Business Logic) → Repository (Database)
Handler Layer (internal/handlers/)
Handles HTTP concerns only:
- Parse and validate request parameters
- Call the service
- Map service errors to HTTP responses
- Encode JSON responses
func (h *FileHandler) ListFiles(w http.ResponseWriter, r *http.Request) {
params, appErr := parseListFilesParams(r)
if appErr != nil {
apperrors.RespondWithError(w, appErr)
return
}
files, nextCursor, err := h.service.ListFiles(r.Context(), *params)
if err != nil {
appErr := apperrors.ToHTTPError(err)
apperrors.RespondWithError(w, appErr)
return
}
respondJSON(w, http.StatusOK, response)
}
Service Layer (internal/<domain>/service.go)
Contains business logic and data transformation:
- Validates business rules
- Orchestrates repository calls
- Converts between domain models and database types
The service depends on a Repository interface, not a concrete implementation.
Repository Layer (internal/<domain>/sqlc_repository.go)
Thin adapter between the service and sqlc-generated code:
- Calls sqlc-generated queries
- Maps database rows to domain models
Interface-Driven Design
Each layer defines the interface it depends on in the consuming package:
// In handlers/ — defines what it needs from the service
type FileService interface {
GetFile(ctx context.Context, params files.GetFileParams) (files.File, error)
ListFiles(ctx context.Context, params files.ListFilesParams) ([]files.File, *string, error)
}
This allows mock generation with mockery for unit testing:
make mockery
Mocks are generated into internal/<domain>/mocks/.
Error Handling (pkg/apperrors)
All API errors use the structured AppError type:
type AppError struct {
Code int `json:"code"`
Status string `json:"status"`
Message string `json:"message"`
Details map[string]string `json:"details,omitempty"`
}
Error Factories
| Function | HTTP Code | When |
|---|---|---|
NewBadRequest(msg, details) | 400 | Invalid input with field-level errors |
NewUnauthorized() | 401 | Missing or invalid auth |
NewForbidden() | 403 | Valid auth but insufficient permissions |
NewNotFound(msg) | 404 | Resource not found |
NewInternalError() | 500 | Unexpected failures |
Error Flow
- Service returns a sentinel error (e.g.
apperrors.ErrNotFound) - Handler calls
apperrors.ToHTTPError(err)to map it to anAppError - Handler calls
apperrors.RespondWithError(w, appErr)to write the JSON response - 5xx errors are logged with
slog.Errorfor observability
sqlc Workflow
We use sqlc to generate type-safe Go code from SQL queries.
- Write SQL queries in
db/queries/<domain>.sqlusing sqlc annotations - Run
sqlc generate(configured insqlc.yml) - Generated code appears in
internal/db/ - Repository adapters in
internal/<domain>/sqlc_repository.gocall the generated functions
Adding a New Endpoint
- Write the SQL query in
db/queries/<domain>.sql - Run
sqlc generateto create the Go database functions - Add Repository method in
internal/<domain>/sqlc_repository.go - Add Repository interface method in
internal/<domain>/service.go - Write Service method with business logic
- Write Handler method with HTTP parsing and response
- Register the route in
cmd/api/main.go - Generate mocks with
make mockery - Write tests for handler and service