7.2 KiB
Feature Development Guide
This guide documents the end-to-end workflow for building features in your composable monorepo.
Quick Start
# 1. Create feature branch
git checkout -b feature/my-feature
# 2. Implement API endpoint (use Wrap + Bind patterns)
# 3. Implement frontend (use design system + auth)
# 4. Write tests
# 5. Run quality checks
./scripts/quality.sh
# 6. Commit and push
git add -A && git commit -m "feat: my feature"
git push -u origin feature/my-feature
Architecture Overview
sp2-verify-1770321984/
├── pkg/ # Shared Go packages (chassis)
│ ├── app/ # Wrap, Bind, Health patterns
│ ├── httperror/ # Typed HTTP errors
│ ├── httpresponse/ # JSON response envelope
│ └── auth/ # JWT + API key validation
├── packages/ # Shared TypeScript packages
│ ├── ui/ # Design system components
│ ├── layout/ # DashboardShell, Sidebar
│ ├── auth/ # AuthProvider, useAuth
│ └── api-client/ # Generated TypeScript client
├── services/ # Go backend services
└── apps/ # Frontend applications
Backend Patterns
Wrap Pattern
Convert error-returning handlers to http.HandlerFunc:
import "sp2-verify-1770321984/pkg/app"
func (h *Handler) GetItem() http.HandlerFunc {
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
// Return errors - they become proper HTTP responses
item, err := h.svc.Get(r.Context(), chi.URLParam(r, "id"))
if err != nil {
return httperror.NotFound("item not found")
}
httpresponse.JSON(w, http.StatusOK, item)
return nil
})
}
Bind Pattern
Parse and validate request bodies in one call:
import "sp2-verify-1770321984/pkg/app"
func (h *Handler) CreateItem() http.HandlerFunc {
type request struct {
Name string `json:"name" validate:"required,min=1,max=100"`
Type string `json:"type" validate:"required,oneof=a b c"`
}
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
var req request
if err := app.BindAndValidate(r, &req); err != nil {
return err // Validation errors become 400 with details
}
// Use req.Name, req.Type...
})
}
HTTPError Sentinels
Use typed errors that map to HTTP status codes:
import "sp2-verify-1770321984/pkg/httperror"
httperror.BadRequest("invalid input") // 400
httperror.Unauthorized("not authenticated") // 401
httperror.Forbidden("access denied") // 403
httperror.NotFound("resource not found") // 404
httperror.Conflict("already exists") // 409
httperror.Internal("something went wrong") // 500
// With formatted messages
httperror.NotFoundf("user %s not found", userID)
// With details
httperror.WithDetails(
httperror.BadRequest("validation failed"),
map[string]string{"name": "required"},
)
Auth Context
Access the authenticated user from request context:
import "sp2-verify-1770321984/pkg/auth"
user, ok := auth.GetUser(r.Context())
if !ok {
return httperror.Unauthorized("not authenticated")
}
// user.ID, user.Email, user.Roles available
Frontend Patterns
Design System Components
Import from the shared UI package:
import { Button, Card, Input, Label, Badge } from '@sp2-verify-1770321984/ui';
import { DashboardShell, Sidebar, Header } from '@sp2-verify-1770321984/layout';
Auth Hook
Access auth state and actions:
import { useAuth } from '@sp2-verify-1770321984/auth';
function MyComponent() {
const { user, isLoading, login, logout, refreshUser } = useAuth();
if (isLoading) return <div>Loading...</div>;
if (!user) return <LoginPrompt />;
return <div>Hello, {user.name}!</div>;
}
Protected Routes
Wrap routes that require authentication:
import { ProtectedRoute } from '@sp2-verify-1770321984/auth';
// In your router or layout
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
Server Actions (Next.js)
Create server actions for form submissions:
// src/actions/my-action.ts
'use server';
import { revalidatePath } from 'next/cache';
import { cookies } from 'next/headers';
export async function createItem(formData: FormData) {
const token = cookies().get('auth_token')?.value;
const response = await fetch(`${process.env.API_URL}/api/items`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({
name: formData.get('name'),
}),
});
if (!response.ok) {
const error = await response.json();
return { success: false, error: error.message };
}
revalidatePath('/items');
return { success: true };
}
API Client Generation
After adding OpenAPI annotations, regenerate the TypeScript client:
./scripts/generate-client.sh
This updates packages/api-client/src/schema.d.ts with typed endpoints.
Testing
Handler Tests
func TestHandler_GetItem(t *testing.T) {
handler := NewHandler(mockService, slog.Default())
req := httptest.NewRequest("GET", "/api/items/123", nil)
req = req.WithContext(auth.SetUser(req.Context(), &auth.User{ID: "user-1"}))
rr := httptest.NewRecorder()
handler.GetItem().ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected 200, got %d: %s", rr.Code, rr.Body.String())
}
}
Running Tests
# All tests
./scripts/quality.sh
# Specific service
go test ./services/api/...
# With coverage
go test -cover ./services/api/...
Code Review
Use the built-in review command:
/review-code
This checks for:
- Completeness and accuracy
- Tech debt indicators
- Maintainability issues
- DRY/CLEAN violations
Deployment
Commit Message Format
feat: add user profile feature
- Add GET/PUT /api/users/me endpoints
- Add profile page with edit form
- Add handler tests
CI Pipeline
On push to any branch:
- Build all components
- Run tests
- Build Docker images
On merge to main:
- Build + test
- Push to registry
- Deploy to K8s
Verifying Deployment
# Check pod status
kubectl get pods -n projects -l app=sp2-verify-1770321984
# View logs
kubectl logs -n projects -l app=sp2-verify-1770321984 -f
# Test endpoint
curl https://ed7qzjfc.threesix.ai/api/health
Common Issues
Handler returns 500 instead of proper error
Use httperror.* functions, not raw errors.New().
Validation errors missing details
Use app.BindAndValidate() instead of separate bind + validate.
Auth middleware not working
Check route is inside the r.Group() with auth.Middleware().
Generated client out of sync
Run ./scripts/generate-client.sh after OpenAPI changes.
Frontend auth not working
Ensure AuthProvider wraps your app in providers.tsx.