# 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="" ``` ### 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 (
Profile Settings
); } ``` ### 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(null); if (!user) { return
Loading...
; } 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 (
setName(e.target.value)} placeholder="Enter your name" required />
setAvatarUrl(e.target.value)} placeholder="https://example.com/avatar.png" />
{error && (
{error}
)}
); } ``` ### 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 { 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)