Implements deterministic feature lifecycle management for agent-driven
development. Agents use the CLI in pods; operators control via REST API.
Library (internal/sdlc/):
- Feature lifecycle with 10 phases (draft → released)
- Classifier engine with priority-ordered rules
- Artifact tracking with approval workflow
- Task management within features
- YAML-based state persistence
CLI (cmd/sdlc/):
- init, state, next, feature, artifact, task, query commands
- --json flag for machine-readable output
- Runs inside project pods
API (21 endpoints under /projects/{id}/sdlc/):
- State: GET /state, GET /next
- Features: CRUD + transition/block/unblock
- Artifacts: approve/reject per type
- Tasks: add/start/complete/block
- Queries: blocked/ready/needs-approval
Architecture:
- Port: SDLCExecutor interface (internal/port/)
- Adapter: kubectl exec into pods (internal/adapter/kubernetes/)
- Service: pod resolution + logging (internal/service/)
- Handlers: 5 files under 500-line limit (internal/handlers/)
Also includes template upgrades (chassis framework, UI components,
OpenAPI helpers, backend/frontend guides) and component improvements.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
833 lines
22 KiB
Markdown
833 lines
22 KiB
Markdown
# Feature Development Cookbook
|
|
|
|
> Complete workflow for building features using rdev composable monorepo templates with chassis patterns, design system, and auth integration.
|
|
|
|
## Overview
|
|
|
|
This cookbook documents the end-to-end workflow for developing features in a composable monorepo project. It assumes you have:
|
|
|
|
- An rdev project with monorepo skeleton (created via `POST /project`)
|
|
- At least one API service component (`services/api`)
|
|
- A Next.js or React app component (`apps/dashboard`)
|
|
|
|
```
|
|
Feature Development Workflow
|
|
────────────────────────────
|
|
1. Planning → Define requirements, break into tasks
|
|
2. API Dev → Bind + Wrap patterns, OpenAPI annotations
|
|
3. Frontend Dev → Auth hooks, design system, API client
|
|
4. Testing → Handler tests, integration tests
|
|
5. Review → /review-code, quality checks
|
|
6. Deployment → Git push, Woodpecker CI, verify
|
|
```
|
|
|
|
---
|
|
|
|
## Prerequisites
|
|
|
|
### API Access
|
|
```bash
|
|
export RDEV_API_URL="https://rdev.masq-ops.orchard9.ai"
|
|
export RDEV_API_KEY="<your-api-key>"
|
|
```
|
|
|
|
### Project Structure
|
|
Your composable monorepo should have:
|
|
|
|
```
|
|
my-project/
|
|
├── pkg/ # Shared Go packages
|
|
│ ├── app/ # Service bootstrapper
|
|
│ │ ├── app.go # NewApp, Run, RegisterRoutes
|
|
│ │ ├── handler.go # Wrap pattern
|
|
│ │ ├── bind.go # Bind + BindAndValidate
|
|
│ │ └── health.go # Health probes
|
|
│ ├── httperror/ # Typed HTTP errors
|
|
│ ├── httpresponse/ # JSON response envelope
|
|
│ ├── httpvalidation/ # go-playground/validator
|
|
│ └── auth/ # JWT + API key validation
|
|
├── packages/ # Shared TS packages
|
|
│ ├── ui/ # Design system components
|
|
│ ├── layout/ # DashboardShell, Sidebar
|
|
│ ├── auth/ # AuthProvider, useAuth
|
|
│ ├── logger/ # Structured logging
|
|
│ └── api-client/ # Generated TypeScript client
|
|
├── services/ # Go backend services
|
|
│ └── api/
|
|
│ ├── cmd/server/main.go
|
|
│ └── internal/api/routes.go
|
|
└── apps/ # Frontend applications
|
|
└── dashboard/
|
|
└── src/app/ # Next.js App Router
|
|
```
|
|
|
|
---
|
|
|
|
## Step 1: Feature Planning
|
|
|
|
### Create Feature Branch
|
|
```bash
|
|
git checkout -b feature/user-profile
|
|
```
|
|
|
|
### Define Requirements
|
|
|
|
Example: User Profile Feature
|
|
- **Backend:** GET/PUT /api/users/me endpoints
|
|
- **Frontend:** Profile page with edit form
|
|
- **Validation:** Email format, name required
|
|
- **Auth:** Require authenticated user
|
|
|
|
### Break Into Tasks
|
|
|
|
```markdown
|
|
## Tasks
|
|
|
|
- [ ] Add User domain model with validation
|
|
- [ ] Implement GET /api/users/me handler
|
|
- [ ] Implement PUT /api/users/me handler
|
|
- [ ] Add OpenAPI annotations
|
|
- [ ] Create profile page component
|
|
- [ ] Add profile edit form
|
|
- [ ] Connect to API with generated client
|
|
- [ ] Write handler tests
|
|
- [ ] Run quality checks
|
|
```
|
|
|
|
---
|
|
|
|
## Step 2: API Development
|
|
|
|
### 2.1 Add Domain Model
|
|
|
|
Create the domain model in `services/api/internal/domain/`:
|
|
|
|
```go
|
|
// services/api/internal/domain/user.go
|
|
package domain
|
|
|
|
import "time"
|
|
|
|
// User represents an authenticated user's profile.
|
|
type User struct {
|
|
ID string `json:"id" db:"id"`
|
|
Email string `json:"email" db:"email"`
|
|
Name string `json:"name" db:"name"`
|
|
AvatarURL string `json:"avatar_url,omitempty" db:"avatar_url"`
|
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
|
}
|
|
```
|
|
|
|
### 2.2 Create Handler with Wrap Pattern
|
|
|
|
The `Wrap` pattern converts error-returning handlers to `http.HandlerFunc`:
|
|
|
|
```go
|
|
// services/api/internal/api/handlers/user.go
|
|
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
|
|
"my-project/pkg/app"
|
|
"my-project/pkg/auth"
|
|
"my-project/pkg/httperror"
|
|
"my-project/pkg/httpresponse"
|
|
"my-project/services/api/internal/service"
|
|
)
|
|
|
|
type UserHandler struct {
|
|
svc *service.UserService
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewUserHandler(svc *service.UserService, logger *slog.Logger) *UserHandler {
|
|
return &UserHandler{svc: svc, logger: logger}
|
|
}
|
|
|
|
// GetMe returns the authenticated user's profile.
|
|
// Uses app.Wrap to convert error-returning handler to http.HandlerFunc.
|
|
func (h *UserHandler) GetMe() http.HandlerFunc {
|
|
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Get user from auth context
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
|
|
// Fetch full profile
|
|
profile, err := h.svc.GetByID(r.Context(), user.ID)
|
|
if err != nil {
|
|
return httperror.WrapError(err, "failed to get profile")
|
|
}
|
|
|
|
httpresponse.JSON(w, http.StatusOK, profile)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UpdateMe updates the authenticated user's profile.
|
|
// Uses app.BindAndValidate for request parsing and validation.
|
|
func (h *UserHandler) UpdateMe() http.HandlerFunc {
|
|
type request struct {
|
|
Name string `json:"name" validate:"required,min=1,max=100"`
|
|
AvatarURL string `json:"avatar_url" validate:"omitempty,url"`
|
|
}
|
|
|
|
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Get user from auth context
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
|
|
// Bind and validate request body
|
|
var req request
|
|
if err := app.BindAndValidate(r, &req); err != nil {
|
|
return err // Already an HTTPError with validation details
|
|
}
|
|
|
|
// Update profile
|
|
profile, err := h.svc.Update(r.Context(), user.ID, req.Name, req.AvatarURL)
|
|
if err != nil {
|
|
return httperror.WrapError(err, "failed to update profile")
|
|
}
|
|
|
|
httpresponse.JSON(w, http.StatusOK, profile)
|
|
return nil
|
|
})
|
|
}
|
|
```
|
|
|
|
### 2.3 Register Routes
|
|
|
|
```go
|
|
// services/api/internal/api/routes.go
|
|
func (s *Server) RegisterRoutes() {
|
|
userHandler := handlers.NewUserHandler(s.userService, s.logger)
|
|
|
|
// Protected routes (require auth)
|
|
s.router.Group(func(r chi.Router) {
|
|
r.Use(auth.Middleware(s.authValidator))
|
|
|
|
r.Get("/api/users/me", userHandler.GetMe())
|
|
r.Put("/api/users/me", userHandler.UpdateMe())
|
|
})
|
|
}
|
|
```
|
|
|
|
### 2.4 Add OpenAPI Annotations
|
|
|
|
```go
|
|
// services/api/cmd/server/main.go
|
|
func buildOpenAPISpec() *api.OpenAPISpec {
|
|
spec := api.NewOpenAPISpec("My Project API", "1.0.0", "/api")
|
|
|
|
// Security schemes
|
|
spec.WithBearerSecurity("bearerAuth", "JWT Bearer token")
|
|
spec.WithGlobalSecurity("bearerAuth")
|
|
|
|
// User profile endpoints
|
|
spec.Op("GET", "/users/me", "Get current user profile",
|
|
api.OpResponses(
|
|
api.OpResponse(200, "User profile", api.Ref("User")),
|
|
api.OpStandardResponses()...,
|
|
),
|
|
)
|
|
|
|
spec.OpWithBody("PUT", "/users/me", "Update current user profile",
|
|
api.Object(map[string]*api.Schema{
|
|
"name": api.String().Description("Display name"),
|
|
"avatar_url": api.String().Format("uri").Description("Avatar URL"),
|
|
}, "name"),
|
|
api.OpResponses(
|
|
api.OpResponse(200, "Updated user profile", api.Ref("User")),
|
|
api.OpStandardResponses()...,
|
|
),
|
|
)
|
|
|
|
// Schema definitions
|
|
spec.Schema("User", api.Object(map[string]*api.Schema{
|
|
"id": api.UUID(),
|
|
"email": api.Email(),
|
|
"name": api.String(),
|
|
"avatar_url": api.String().Format("uri"),
|
|
"created_at": api.DateTime(),
|
|
"updated_at": api.DateTime(),
|
|
}))
|
|
|
|
return spec
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Step 3: Frontend Development
|
|
|
|
### 3.1 Generate TypeScript Client
|
|
|
|
After adding OpenAPI annotations, regenerate the client:
|
|
|
|
```bash
|
|
./scripts/generate-client.sh
|
|
```
|
|
|
|
This updates `packages/api-client/src/schema.d.ts` with typed endpoints.
|
|
|
|
### 3.2 Create Profile Page
|
|
|
|
```tsx
|
|
// apps/dashboard/src/app/(dashboard)/profile/page.tsx
|
|
import { ProfileForm } from '@/components/profile-form';
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@my-project/ui';
|
|
|
|
export default function ProfilePage() {
|
|
return (
|
|
<div className="p-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profile Settings</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<ProfileForm />
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.3 Create Profile Form with Auth Hook
|
|
|
|
```tsx
|
|
// apps/dashboard/src/components/profile-form.tsx
|
|
'use client';
|
|
|
|
import { useState } from 'react';
|
|
import { useAuth } from '@my-project/auth';
|
|
import { Button, Input, Label } from '@my-project/ui';
|
|
import { updateProfile } from '@/actions/profile';
|
|
|
|
export function ProfileForm() {
|
|
const { user, refreshUser } = useAuth();
|
|
const [name, setName] = useState(user?.name ?? '');
|
|
const [avatarUrl, setAvatarUrl] = useState(user?.avatar_url ?? '');
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (!user) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await updateProfile({ name, avatar_url: avatarUrl });
|
|
if (result.success) {
|
|
await refreshUser();
|
|
} else {
|
|
setError(result.error ?? 'Failed to update profile');
|
|
}
|
|
} catch (err) {
|
|
setError('An unexpected error occurred');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={user.email}
|
|
disabled
|
|
className="bg-surface-50"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Display Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Enter your name"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="avatar">Avatar URL</Label>
|
|
<Input
|
|
id="avatar"
|
|
type="url"
|
|
value={avatarUrl}
|
|
onChange={(e) => setAvatarUrl(e.target.value)}
|
|
placeholder="https://example.com/avatar.png"
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="text-sm text-error">{error}</div>
|
|
)}
|
|
|
|
<Button type="submit" disabled={isSubmitting}>
|
|
{isSubmitting ? 'Saving...' : 'Save Changes'}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|
|
```
|
|
|
|
### 3.4 Create Server Action
|
|
|
|
```typescript
|
|
// apps/dashboard/src/actions/profile.ts
|
|
'use server';
|
|
|
|
import { revalidatePath } from 'next/cache';
|
|
import { cookies } from 'next/headers';
|
|
|
|
interface UpdateProfileInput {
|
|
name: string;
|
|
avatar_url?: string;
|
|
}
|
|
|
|
interface UpdateProfileResult {
|
|
success: boolean;
|
|
error?: string;
|
|
}
|
|
|
|
export async function updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult> {
|
|
const token = cookies().get('auth_token')?.value;
|
|
|
|
if (!token) {
|
|
return { success: false, error: 'Not authenticated' };
|
|
}
|
|
|
|
const response = await fetch(`${process.env.API_URL}/api/users/me`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`,
|
|
},
|
|
body: JSON.stringify(input),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
return {
|
|
success: false,
|
|
error: error.error?.message ?? 'Failed to update profile',
|
|
};
|
|
}
|
|
|
|
revalidatePath('/profile');
|
|
return { success: true };
|
|
}
|
|
```
|
|
|
|
### 3.5 Add Navigation Link
|
|
|
|
```tsx
|
|
// apps/dashboard/src/components/sidebar-nav.tsx
|
|
import { User, Settings, Home } from 'lucide-react';
|
|
|
|
const navItems = [
|
|
{ label: 'Dashboard', href: '/dashboard', icon: Home },
|
|
{ label: 'Profile', href: '/profile', icon: User },
|
|
{ label: 'Settings', href: '/settings', icon: Settings },
|
|
];
|
|
```
|
|
|
|
---
|
|
|
|
## Step 4: Testing
|
|
|
|
### 4.1 Write Handler Tests
|
|
|
|
```go
|
|
// services/api/internal/api/handlers/user_test.go
|
|
package handlers_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"my-project/pkg/auth"
|
|
"my-project/services/api/internal/api/handlers"
|
|
"my-project/services/api/internal/service"
|
|
)
|
|
|
|
func TestUserHandler_GetMe(t *testing.T) {
|
|
svc := service.NewUserService(/* mock deps */)
|
|
handler := handlers.NewUserHandler(svc, slog.Default())
|
|
|
|
t.Run("returns user profile", func(t *testing.T) {
|
|
// Create request with auth context
|
|
req := httptest.NewRequest("GET", "/api/users/me", nil)
|
|
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
|
|
ID: "user-123",
|
|
Email: "test@example.com",
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.GetMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(rr.Body.Bytes(), &resp)
|
|
|
|
data := resp["data"].(map[string]any)
|
|
if data["id"] != "user-123" {
|
|
t.Errorf("expected user-123, got %v", data["id"])
|
|
}
|
|
})
|
|
|
|
t.Run("returns 401 without auth", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "/api/users/me", nil)
|
|
rr := httptest.NewRecorder()
|
|
|
|
handler.GetMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusUnauthorized {
|
|
t.Errorf("expected 401, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUserHandler_UpdateMe(t *testing.T) {
|
|
svc := service.NewUserService(/* mock deps */)
|
|
handler := handlers.NewUserHandler(svc, slog.Default())
|
|
|
|
t.Run("updates profile", func(t *testing.T) {
|
|
body := `{"name": "New Name", "avatar_url": "https://example.com/avatar.png"}`
|
|
req := httptest.NewRequest("PUT", "/api/users/me", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
|
|
ID: "user-123",
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.UpdateMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("validates required name", func(t *testing.T) {
|
|
body := `{"name": ""}`
|
|
req := httptest.NewRequest("PUT", "/api/users/me", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{
|
|
ID: "user-123",
|
|
}))
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.UpdateMe().ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusBadRequest {
|
|
t.Errorf("expected 400, got %d", rr.Code)
|
|
}
|
|
})
|
|
}
|
|
```
|
|
|
|
### 4.2 Run Tests
|
|
|
|
```bash
|
|
# Run all tests
|
|
./scripts/quality.sh
|
|
|
|
# Run specific service tests
|
|
go test ./services/api/...
|
|
|
|
# Run with coverage
|
|
go test -cover ./services/api/...
|
|
```
|
|
|
|
---
|
|
|
|
## Step 5: Review & Quality Checks
|
|
|
|
### 5.1 Run Code Review
|
|
|
|
Use the built-in review command:
|
|
|
|
```bash
|
|
# In Claude Code
|
|
/review-code
|
|
```
|
|
|
|
This checks for:
|
|
- Completeness and accuracy
|
|
- Tech debt indicators
|
|
- Maintainability issues
|
|
- Extensibility concerns
|
|
- DRY/CLEAN violations
|
|
|
|
### 5.2 Run Quality Checks
|
|
|
|
```bash
|
|
# Lint Go code
|
|
golangci-lint run ./...
|
|
|
|
# Lint frontend
|
|
cd apps/dashboard && npm run lint
|
|
|
|
# Type check
|
|
cd apps/dashboard && npm run typecheck
|
|
```
|
|
|
|
### 5.3 Verify OpenAPI Spec
|
|
|
|
```bash
|
|
# Start the service locally
|
|
cd services/api && make run
|
|
|
|
# Check the spec
|
|
curl http://localhost:8001/openapi.json | jq '.paths["/users/me"]'
|
|
|
|
# View docs
|
|
open http://localhost:8001/docs
|
|
```
|
|
|
|
---
|
|
|
|
## Step 6: Deployment
|
|
|
|
### 6.1 Commit Changes
|
|
|
|
```bash
|
|
# Stage changes
|
|
git add services/api/internal/api/handlers/user.go
|
|
git add services/api/internal/api/routes.go
|
|
git add apps/dashboard/src/app/\(dashboard\)/profile/
|
|
git add apps/dashboard/src/components/profile-form.tsx
|
|
git add apps/dashboard/src/actions/profile.ts
|
|
|
|
# Commit
|
|
git commit -m "feat: add user profile feature
|
|
|
|
- Add GET/PUT /api/users/me endpoints with Wrap pattern
|
|
- Add profile page with edit form
|
|
- Add server action for profile updates
|
|
- Add handler tests"
|
|
```
|
|
|
|
### 6.2 Push and Monitor CI
|
|
|
|
```bash
|
|
# Push feature branch
|
|
git push -u origin feature/user-profile
|
|
|
|
# Monitor pipeline
|
|
./cookbooks/scripts/feature-test.sh status my-project
|
|
```
|
|
|
|
### 6.3 Create Pull Request
|
|
|
|
```bash
|
|
gh pr create --title "feat: add user profile feature" --body "
|
|
## Summary
|
|
- Added user profile endpoints (GET/PUT /api/users/me)
|
|
- Added profile page with edit form
|
|
- Integrated with auth system
|
|
|
|
## Testing
|
|
- Handler tests added
|
|
- Manual testing complete
|
|
|
|
## Checklist
|
|
- [x] Handler tests pass
|
|
- [x] Quality checks pass
|
|
- [x] OpenAPI spec updated
|
|
"
|
|
```
|
|
|
|
### 6.4 Verify Deployment
|
|
|
|
After merge to main:
|
|
|
|
```bash
|
|
# Check deployment
|
|
curl https://my-project.threesix.ai/api/users/me \
|
|
-H "Authorization: Bearer $TOKEN" | jq .
|
|
|
|
# Open the app
|
|
open https://my-project.threesix.ai/profile
|
|
```
|
|
|
|
---
|
|
|
|
## Pattern Quick Reference
|
|
|
|
### Wrap Pattern
|
|
```go
|
|
app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Return errors, they become proper HTTP responses
|
|
if err := doThing(); err != nil {
|
|
return httperror.WrapError(err, "failed to do thing")
|
|
}
|
|
httpresponse.JSON(w, http.StatusOK, result)
|
|
return nil
|
|
})
|
|
```
|
|
|
|
### Bind Pattern
|
|
```go
|
|
var req RequestType
|
|
if err := app.BindAndValidate(r, &req); err != nil {
|
|
return err // Validation errors become 400 with details
|
|
}
|
|
```
|
|
|
|
### HTTPError Sentinels
|
|
```go
|
|
httperror.BadRequest("invalid input")
|
|
httperror.NotFound("user not found")
|
|
httperror.Unauthorized("not authenticated")
|
|
httperror.Forbidden("access denied")
|
|
httperror.Conflict("already exists")
|
|
httperror.Internal("something went wrong")
|
|
httperror.Validation("validation failed")
|
|
```
|
|
|
|
### Auth Context
|
|
```go
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
```
|
|
|
|
### React Auth Hook
|
|
```tsx
|
|
const { user, isLoading, login, logout } = useAuth();
|
|
```
|
|
|
|
### Design System Components
|
|
```tsx
|
|
import { Button, Card, Input, Label, Badge } from '@my-project/ui';
|
|
import { DashboardShell, Sidebar, Header } from '@my-project/layout';
|
|
```
|
|
|
|
---
|
|
|
|
## E2E Test Script
|
|
|
|
Validate the complete feature development flow:
|
|
|
|
```bash
|
|
# Create test project with components
|
|
./cookbooks/scripts/feature-test.sh run test-feature
|
|
|
|
# Check status
|
|
./cookbooks/scripts/feature-test.sh status test-feature
|
|
|
|
# Cleanup
|
|
./cookbooks/scripts/feature-test.sh teardown test-feature
|
|
```
|
|
|
|
---
|
|
|
|
## Troubleshooting
|
|
|
|
### Handler returns 500 instead of proper error
|
|
Check that you're using `httperror.*` functions, not raw `errors.New()`.
|
|
|
|
### Validation errors not showing details
|
|
Ensure you're using `app.BindAndValidate()` which includes validation details.
|
|
|
|
### Auth middleware not applying
|
|
Verify route is inside the `r.Group()` with `auth.Middleware()`.
|
|
|
|
### Generated client out of sync
|
|
Run `./scripts/generate-client.sh` after OpenAPI changes.
|
|
|
|
### Frontend not seeing auth user
|
|
Check that `AuthProvider` wraps your app in `providers.tsx`.
|
|
|
|
---
|
|
|
|
## Chassis Framework
|
|
|
|
The `pkg/chassis` package provides a convenience facade over `pkg/app`:
|
|
|
|
```go
|
|
import "my-project/pkg/chassis"
|
|
|
|
svc := chassis.New("my-service", chassis.WithDefaultPort(8080))
|
|
```
|
|
|
|
It re-exports: `New`, `Wrap`, `WrapWithLogger`, `Bind`, `BindAndValidate`, `BindStrict`, `NewHealthHandler`, `PingChecker`, `HTTPChecker`.
|
|
|
|
## OpenAPI Documentation
|
|
|
|
Each service defines its spec in `internal/api/spec.go`:
|
|
|
|
```go
|
|
spec := openapi.NewOpenAPISpec("My API", "1.0.0").
|
|
WithBearerSecurity("bearer", "JWT token")
|
|
|
|
spec.WithSchema("User", openapi.Object(map[string]openapi.Schema{
|
|
"id": openapi.UUID(),
|
|
"email": openapi.Email(),
|
|
}, "id", "email"))
|
|
|
|
spec.AddPath("/api/v1/users/{id}", "get", map[string]any{
|
|
"summary": "Get user",
|
|
"tags": []string{"Users"},
|
|
"parameters": []any{openapi.IDParam()},
|
|
"responses": map[string]any{
|
|
"200": openapi.OpResponse("Success", openapi.ResponseSchema(openapi.Ref("User"))),
|
|
"404": openapi.OpResponse("Not found", openapi.ErrorResponseSchema()),
|
|
},
|
|
})
|
|
|
|
application.EnableDocs(spec) // Mounts /docs (Scalar UI) and /openapi.json
|
|
```
|
|
|
|
## Design System Components
|
|
|
|
Available from `@project/ui`:
|
|
|
|
| Component | Usage |
|
|
|-----------|-------|
|
|
| Button | Primary actions, variants: default, destructive, outline, ghost |
|
|
| Card | Content containers with CardHeader, CardContent, CardFooter |
|
|
| Input, Label | Form fields |
|
|
| Badge | Status indicators, variants: success, warning, error, info |
|
|
| Dialog | Modal dialogs |
|
|
| Table | Data tables |
|
|
| Select | Dropdowns |
|
|
| Alert | Notification banners, variants: default, destructive, success, warning |
|
|
| Textarea | Multiline input |
|
|
| DropdownMenu | Context menus with items, checkboxes, radio groups |
|
|
| Sheet | Slide-in panels (side: top, right, bottom, left) |
|
|
|
|
All use CSS custom properties: `var(--background)`, `var(--accent)`, `var(--border)`, etc.
|
|
|
|
---
|
|
|
|
## Related
|
|
|
|
- [Composable App Cookbook](./composable-app.md) - Creating projects with components
|
|
- [Landing Page Cookbook](./landing-page.md) - Simple single-component sites
|
|
- [Composable Monorepo Guide](../.claude/guides/services/composable-monorepo.md)
|
|
- [API Framework Guide](../.claude/guides/packages/api-framework.md)
|