slate-v3-1770514618/.claude/guides/feature-development.md
jordan c65d01ffe7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-08 01:36:59 +00:00

7.1 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

slate-v3-1770514618/
├── 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 "slate-v3-1770514618/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 "slate-v3-1770514618/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 "slate-v3-1770514618/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 "slate-v3-1770514618/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 '@slate-v3-1770514618/ui';
import { DashboardShell, Sidebar, Header } from '@slate-v3-1770514618/layout';

Auth Hook

Access auth state and actions:

import { useAuth } from '@slate-v3-1770514618/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 '@slate-v3-1770514618/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:

  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

# Check pod status
kubectl get pods -n projects -l app=slate-v3-1770514618

# View logs
kubectl logs -n projects -l app=slate-v3-1770514618 -f

# Test endpoint
curl https://h4rw6uzr.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.