rdev/pkg/api/response.go
jordan 8282d60c69 feat: implement composable monorepo template system with component architecture
Adds the composable monorepo template system that generates project skeletons
with pluggable components (service, worker, app-react, app-astro, cli).

Key changes:
- Monorepo skeleton templates with shared pkg/, scripts/, and git hooks
- Component templates (service, worker, app-react, app-astro, cli) with
  Dockerfiles, CI steps, and component.yaml manifests
- Component domain model with validation and dependency resolution
- Component handler endpoints for CRUD and composition
- Template provider extended with BuildComposableProject and component assembly
- Deployer extended with composable project deployment support
- Handler timeout constants (TimeoutFastLookup through TimeoutLongRunning)
- envutil package for centralized env var reads with defaults
- api.DecodeJSON helper for standardized request body decoding
- Standardized response helpers (WriteBadRequest, WriteNotFound, etc.)
- Replaced fullstack-app cookbook with composable-app cookbook
- Hardened handler timeouts, logging, and error responses across all handlers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 19:11:42 -07:00

110 lines
3.2 KiB
Go

package api
import (
"encoding/json"
"net/http"
"time"
"github.com/go-chi/chi/v5/middleware"
)
// Response is the standard envelope for all API responses.
type Response struct {
Data any `json:"data,omitempty"`
Error *Error `json:"error,omitempty"`
Meta Meta `json:"meta"`
}
// Error represents an API error.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details []any `json:"details,omitempty"`
}
// Meta contains response metadata.
type Meta struct {
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
}
// newMeta creates a Meta with current timestamp and request ID from context.
func newMeta(r *http.Request) Meta {
return Meta{
RequestID: middleware.GetReqID(r.Context()),
Timestamp: time.Now().UTC().Format(time.RFC3339),
}
}
// WriteJSON writes a JSON response with the given status code.
func WriteJSON(w http.ResponseWriter, r *http.Request, status int, data any) {
resp := Response{
Data: data,
Meta: newMeta(r),
}
writeResponse(w, status, resp)
}
// WriteSuccess writes a successful JSON response with status 200 OK.
func WriteSuccess(w http.ResponseWriter, r *http.Request, data any) {
WriteJSON(w, r, http.StatusOK, data)
}
// WriteCreated writes a successful JSON response with status 201 Created.
func WriteCreated(w http.ResponseWriter, r *http.Request, data any) {
WriteJSON(w, r, http.StatusCreated, data)
}
// WriteNoContent writes a successful response with status 204 No Content.
func WriteNoContent(w http.ResponseWriter) {
w.WriteHeader(http.StatusNoContent)
}
// WriteError writes an error response with the given status code.
func WriteError(w http.ResponseWriter, r *http.Request, status int, code, message string, details ...any) {
resp := Response{
Error: &Error{
Code: code,
Message: message,
Details: details,
},
Meta: newMeta(r),
}
writeResponse(w, status, resp)
}
// WriteBadRequest writes a 400 Bad Request error response.
func WriteBadRequest(w http.ResponseWriter, r *http.Request, message string, details ...any) {
WriteError(w, r, http.StatusBadRequest, "BAD_REQUEST", message, details...)
}
// WriteNotFound writes a 404 Not Found error response.
func WriteNotFound(w http.ResponseWriter, r *http.Request, message string) {
WriteError(w, r, http.StatusNotFound, "NOT_FOUND", message)
}
// WriteUnauthorized writes a 401 Unauthorized error response.
func WriteUnauthorized(w http.ResponseWriter, r *http.Request, message string) {
WriteError(w, r, http.StatusUnauthorized, "UNAUTHORIZED", message)
}
// WriteForbidden writes a 403 Forbidden error response.
func WriteForbidden(w http.ResponseWriter, r *http.Request, message string) {
WriteError(w, r, http.StatusForbidden, "FORBIDDEN", message)
}
// WriteInternalError writes a 500 Internal Server Error response.
func WriteInternalError(w http.ResponseWriter, r *http.Request, message string) {
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", message)
}
// writeResponse marshals and writes the response.
func writeResponse(w http.ResponseWriter, status int, resp Response) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(resp); err != nil {
return
}
}