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>
110 lines
3.2 KiB
Go
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
|
|
}
|
|
}
|