315 lines
7.1 KiB
Markdown
315 lines
7.1 KiB
Markdown
# Feature Development Guide
|
|
|
|
This guide documents the end-to-end workflow for building features in your composable monorepo.
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```
|
|
persona-community-5/
|
|
├── 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`:
|
|
|
|
```go
|
|
import "persona-community-5/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:
|
|
|
|
```go
|
|
import "persona-community-5/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:
|
|
|
|
```go
|
|
import "persona-community-5/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:
|
|
|
|
```go
|
|
import "persona-community-5/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:
|
|
|
|
```tsx
|
|
import { Button, Card, Input, Label, Badge } from '@persona-community-5/ui';
|
|
import { DashboardShell, Sidebar, Header } from '@persona-community-5/layout';
|
|
```
|
|
|
|
### Auth Hook
|
|
|
|
Access auth state and actions:
|
|
|
|
```tsx
|
|
import { useAuth } from '@persona-community-5/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:
|
|
|
|
```tsx
|
|
import { ProtectedRoute } from '@persona-community-5/auth';
|
|
|
|
// In your router or layout
|
|
<ProtectedRoute>
|
|
<DashboardPage />
|
|
</ProtectedRoute>
|
|
```
|
|
|
|
### Server Actions (Next.js)
|
|
|
|
Create server actions for form submissions:
|
|
|
|
```typescript
|
|
// 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:
|
|
|
|
```bash
|
|
./scripts/generate-client.sh
|
|
```
|
|
|
|
This updates `packages/api-client/src/schema.d.ts` with typed endpoints.
|
|
|
|
## Testing
|
|
|
|
### Handler Tests
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
/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:
|
|
1. Build all components
|
|
2. Run tests
|
|
3. Build Docker images
|
|
|
|
On merge to main:
|
|
1. Build + test
|
|
2. Push to registry
|
|
3. Deploy to K8s
|
|
|
|
### Verifying Deployment
|
|
|
|
```bash
|
|
# Check pod status
|
|
kubectl get pods -n projects -l app=persona-community-5
|
|
|
|
# View logs
|
|
kubectl logs -n projects -l app=persona-community-5 -f
|
|
|
|
# Test endpoint
|
|
curl https://me4yrdax.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`.
|
|
|
|
## Related
|
|
|
|
- [Local Setup](./local/setup.md)
|
|
- [Deploying](./ops/deploying.md)
|