Weeks 1-7 of the template upgrade plan: - pkg/api: typed HTTPError with sentinels, Wrap/WrapMiddleware, Bind, health probes, OpenAPI schema/param builders - skeleton/packages: ui (design tokens, components), layout (DashboardShell), auth (AuthProvider, ProtectedRoute), api-client - skeleton/pkg: httperror, app/handler, app/bind, app/health, auth (JWT/API key middleware) - components/app-nextjs: Next.js 14 App Router template with dashboard, server actions, auth - cookbooks/feature-development.md with test and validation scripts - Handler tests for components, project management, and woodpecker webhook - 3 rounds of code review fixes applied Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
259 lines
8.0 KiB
Markdown
259 lines
8.0 KiB
Markdown
# Dashboard App Example
|
|
|
|
This example demonstrates a full-stack composable monorepo project using rdev's enhanced templates with:
|
|
|
|
- **Chassis patterns**: Wrap, Bind, HTTPError, Health probes
|
|
- **Design system**: packages/ui, packages/layout
|
|
- **Authentication**: pkg/auth, packages/auth
|
|
- **OpenAPI**: Auto-generated TypeScript client
|
|
- **Next.js 14**: App Router with server actions
|
|
|
|
## Creating This Example
|
|
|
|
```bash
|
|
# 1. Create the project
|
|
curl -X POST "$RDEV_API_URL/project" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"name": "dashboard-app",
|
|
"description": "Example dashboard application"
|
|
}'
|
|
|
|
# 2. Add the API service
|
|
curl -X POST "$RDEV_API_URL/projects/dashboard-app/components" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"type": "service",
|
|
"name": "api"
|
|
}'
|
|
|
|
# 3. Add the Next.js dashboard
|
|
curl -X POST "$RDEV_API_URL/projects/dashboard-app/components" \
|
|
-H "X-API-Key: $RDEV_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"type": "app-nextjs",
|
|
"name": "dashboard"
|
|
}'
|
|
```
|
|
|
|
## Project Structure
|
|
|
|
After creation, the project looks like:
|
|
|
|
```
|
|
dashboard-app/
|
|
├── CLAUDE.md # AI routing with guides
|
|
├── README.md # Project docs
|
|
├── go.work # Go workspace
|
|
├── pnpm-workspace.yaml # pnpm monorepo config
|
|
├── .woodpecker.yml # CI pipeline
|
|
│
|
|
├── pkg/ # Shared Go packages
|
|
│ ├── app/ # Wrap, Bind, Health
|
|
│ │ ├── app.go # NewApp, Run
|
|
│ │ ├── handler.go # Wrap pattern
|
|
│ │ ├── bind.go # BindAndValidate
|
|
│ │ └── health.go # Health probes
|
|
│ ├── httperror/ # Typed HTTP errors
|
|
│ │ └── error.go # Sentinels, factory functions
|
|
│ ├── httpresponse/ # JSON envelope
|
|
│ ├── httpvalidation/ # Validator wrapper
|
|
│ ├── middleware/ # HTTP middleware
|
|
│ └── auth/ # JWT + API key auth
|
|
│ ├── auth.go # Interface, types
|
|
│ ├── context.go # GetUser, SetUser
|
|
│ ├── jwt.go # JWTValidator
|
|
│ ├── apikey.go # APIKeyValidator
|
|
│ └── middleware.go # Auth middleware
|
|
│
|
|
├── packages/ # Shared TypeScript packages
|
|
│ ├── ui/ # Design system
|
|
│ │ ├── src/styles/tokens.css # CSS custom properties
|
|
│ │ ├── src/utils/cn.ts # clsx + tailwind-merge
|
|
│ │ └── src/components/ # Button, Card, Input, etc.
|
|
│ ├── layout/ # Shell components
|
|
│ │ ├── src/DashboardShell.tsx # Main layout
|
|
│ │ ├── src/Sidebar.tsx # Navigation
|
|
│ │ └── src/Header.tsx # Top bar
|
|
│ ├── auth/ # React auth
|
|
│ │ ├── src/AuthProvider.tsx # Context provider
|
|
│ │ ├── src/useAuth.ts # Hook
|
|
│ │ └── src/ProtectedRoute.tsx # Route guard
|
|
│ ├── logger/ # Structured logging
|
|
│ └── api-client/ # Generated client
|
|
│ └── src/schema.d.ts # TypeScript types
|
|
│
|
|
├── services/ # Backend services
|
|
│ └── api/
|
|
│ ├── cmd/server/main.go # Entry point
|
|
│ ├── internal/
|
|
│ │ ├── api/routes.go # Chi routes with Wrap
|
|
│ │ ├── config/config.go # Configuration
|
|
│ │ └── domain/ # Business models
|
|
│ ├── migrations/ # SQL migrations
|
|
│ ├── Dockerfile # Multi-stage Go build
|
|
│ └── component.yaml # Port, dependencies
|
|
│
|
|
└── apps/ # Frontend applications
|
|
└── dashboard/
|
|
├── src/app/
|
|
│ ├── layout.tsx # Root layout
|
|
│ ├── globals.css # Design tokens import
|
|
│ ├── (dashboard)/ # Protected route group
|
|
│ │ ├── layout.tsx # DashboardShell
|
|
│ │ ├── page.tsx # Home
|
|
│ │ └── settings/page.tsx
|
|
│ └── login/page.tsx # Public login
|
|
├── src/components/
|
|
│ └── providers.tsx # AuthProvider wrapper
|
|
├── src/actions/ # Server actions
|
|
├── Dockerfile # Next.js standalone
|
|
└── component.yaml
|
|
```
|
|
|
|
## Development Workflow
|
|
|
|
### Local Development
|
|
|
|
```bash
|
|
# Start infrastructure (Postgres, Redis)
|
|
docker-compose up -d
|
|
|
|
# Install dependencies
|
|
./scripts/install.sh
|
|
|
|
# Start all services
|
|
./scripts/dev.sh
|
|
```
|
|
|
|
### Building a Feature
|
|
|
|
1. **Create feature branch**
|
|
```bash
|
|
git checkout -b feature/user-profile
|
|
```
|
|
|
|
2. **Add API endpoint** using Wrap + Bind:
|
|
```go
|
|
func (h *Handler) GetMe() http.HandlerFunc {
|
|
return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
user, ok := auth.GetUser(r.Context())
|
|
if !ok {
|
|
return httperror.Unauthorized("not authenticated")
|
|
}
|
|
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
|
|
})
|
|
}
|
|
```
|
|
|
|
3. **Regenerate TypeScript client**:
|
|
```bash
|
|
./scripts/generate-client.sh
|
|
```
|
|
|
|
4. **Add frontend page** using design system:
|
|
```tsx
|
|
import { Card, CardHeader, CardTitle } from '@dashboard-app/ui';
|
|
import { useAuth } from '@dashboard-app/auth';
|
|
|
|
export default function ProfilePage() {
|
|
const { user } = useAuth();
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Profile: {user?.name}</CardTitle>
|
|
</CardHeader>
|
|
</Card>
|
|
);
|
|
}
|
|
```
|
|
|
|
5. **Run tests and quality checks**:
|
|
```bash
|
|
./scripts/quality.sh
|
|
```
|
|
|
|
6. **Commit and push**:
|
|
```bash
|
|
git add -A
|
|
git commit -m "feat: add user profile"
|
|
git push -u origin feature/user-profile
|
|
```
|
|
|
|
## Key Patterns
|
|
|
|
### Backend: Wrap Pattern
|
|
|
|
```go
|
|
// Error-returning handlers become http.HandlerFunc
|
|
app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
|
|
// Return errors - they become proper HTTP responses
|
|
if err := doThing(); err != nil {
|
|
return httperror.Internal("something went wrong")
|
|
}
|
|
httpresponse.JSON(w, http.StatusOK, data)
|
|
return nil
|
|
})
|
|
```
|
|
|
|
### Backend: Bind Pattern
|
|
|
|
```go
|
|
// Decode + validate in one call
|
|
var req CreateItemRequest
|
|
if err := app.BindAndValidate(r, &req); err != nil {
|
|
return err // Validation errors become 400 with details
|
|
}
|
|
```
|
|
|
|
### Frontend: Auth Hook
|
|
|
|
```tsx
|
|
const { user, isLoading, login, logout } = useAuth();
|
|
|
|
if (isLoading) return <Spinner />;
|
|
if (!user) return <LoginRedirect />;
|
|
```
|
|
|
|
### Frontend: Design System
|
|
|
|
```tsx
|
|
import { Button, Card, Input, Badge } from '@dashboard-app/ui';
|
|
import { DashboardShell, Sidebar } from '@dashboard-app/layout';
|
|
```
|
|
|
|
## Deployment
|
|
|
|
CI/CD is automated via Woodpecker:
|
|
|
|
1. Push triggers build
|
|
2. Kaniko builds Docker images
|
|
3. Images pushed to registry
|
|
4. kubectl deploys to K8s
|
|
|
|
Access the deployed app:
|
|
```bash
|
|
open https://<slug>.threesix.ai
|
|
```
|
|
|
|
## Cleanup
|
|
|
|
```bash
|
|
curl -X DELETE "$RDEV_API_URL/project/dashboard-app" \
|
|
-H "X-API-Key: $RDEV_API_KEY"
|
|
```
|
|
|
|
## Related Documentation
|
|
|
|
- [Feature Development Cookbook](../../cookbooks/feature-development.md)
|
|
- [Composable App Cookbook](../../cookbooks/composable-app.md)
|
|
- [API Framework Guide](../../.claude/guides/packages/api-framework.md)
|