Deep Full-Stack - Coordinated Client/Server Development
When invoked, coordinate development across the entire stack with shared contracts and comprehensive E2E testing.
The Full-Stack Mindset
- Contracts first - Define the API contract before implementation
- Backend leads - API implementation is the source of truth
- Types flow down - Generate frontend types from backend
- E2E validates - Integration tests verify the full flow
Phase 1: Define Contracts
API Contract Definition
Choose your contract approach:
Option A: OpenAPI/Swagger
# openapi.yaml
openapi: 3.0.0
info:
title: [API Name]
version: 1.0.0
paths:
/users:
get:
summary: List users
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string
Option B: tRPC (TypeScript)
// src/server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure } from '../trpc';
export const userRouter = router({
list: publicProcedure
.query(async () => {
return await db.users.findMany();
}),
create: publicProcedure
.input(z.object({
email: z.string().email(),
name: z.string().min(1),
}))
.mutation(async ({ input }) => {
return await db.users.create({ data: input });
}),
});
Option C: Shared Types (Monorepo)
// packages/shared/types/user.ts
export interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
export interface CreateUserInput {
email: string;
name: string;
}
export interface UserResponse {
data: User;
}
export interface UsersListResponse {
data: User[];
pagination: {
total: number;
page: number;
limit: number;
};
}
Error Codes Contract
// packages/shared/errors.ts
export const ErrorCodes = {
VALIDATION_ERROR: 'VALIDATION_ERROR',
NOT_FOUND: 'NOT_FOUND',
UNAUTHORIZED: 'UNAUTHORIZED',
FORBIDDEN: 'FORBIDDEN',
CONFLICT: 'CONFLICT',
INTERNAL_ERROR: 'INTERNAL_ERROR',
} as const;
export interface ApiError {
code: keyof typeof ErrorCodes;
message: string;
details?: Record<string, string[]>;
}
Phase 2: Build Backend
Implementation Order
- Database schema/migrations
- Models/entities
- Business logic (services)
- API routes/controllers
- Validation middleware
- Error handling
Backend Checklist
- All endpoints match contract
- Request validation enforced
- Response shapes match types
- Error responses follow contract
- Unit tests pass
- Integration tests pass
Generate Client Types
# From OpenAPI
npx openapi-typescript openapi.yaml --output src/types/api.ts
# From tRPC (automatic)
# Types are inferred from router
# From shared package (monorepo)
# Import directly: import { User } from '@myapp/shared'
Phase 3: Build Frontend
Implementation Order
- API client setup
- Type imports/generation
- Data fetching hooks
- UI components
- Forms with validation
- Error handling UI
API Client Setup
Fetch wrapper:
// src/lib/api.ts
import type { ApiError } from '@myapp/shared';
const API_BASE = process.env.NEXT_PUBLIC_API_URL;
export async function api<T>(
endpoint: string,
options?: RequestInit
): Promise<T> {
const res = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
});
if (!res.ok) {
const error: ApiError = await res.json();
throw new ApiClientError(error);
}
return res.json();
}
React Query hooks:
// src/hooks/useUsers.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import type { User, CreateUserInput } from '@myapp/shared';
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: () => api<{ data: User[] }>('/users'),
});
}
export function useCreateUser() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreateUserInput) =>
api<{ data: User }>('/users', {
method: 'POST',
body: JSON.stringify(input),
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['users'] });
},
});
}
Frontend Checklist
- Types imported from backend/shared
- API calls use typed client
- Loading states handled
- Error states handled
- Form validation matches backend
- Component tests pass
Phase 4: Integration Testing
E2E Test Setup
Playwright:
// e2e/users.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User Management', () => {
test('can create a new user', async ({ page }) => {
await page.goto('/users/new');
await page.fill('[name="email"]', 'test@example.com');
await page.fill('[name="name"]', 'Test User');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/users');
await expect(page.locator('text=Test User')).toBeVisible();
});
test('shows validation errors', async ({ page }) => {
await page.goto('/users/new');
await page.click('button[type="submit"]');
await expect(page.locator('text=Email is required')).toBeVisible();
await expect(page.locator('text=Name is required')).toBeVisible();
});
});
Cypress:
// cypress/e2e/users.cy.ts
describe('User Management', () => {
it('can create a new user', () => {
cy.visit('/users/new');
cy.get('[name="email"]').type('test@example.com');
cy.get('[name="name"]').type('Test User');
cy.get('button[type="submit"]').click();
cy.url().should('include', '/users');
cy.contains('Test User').should('be.visible');
});
});
Run E2E Tests
# Playwright
npx playwright test 2>&1
# Cypress
npx cypress run 2>&1
E2E Checklist
- Happy path works end-to-end
- Form validation works
- Error handling works
- Navigation works
- Authentication flow works
- Data persists correctly
Phase 5: Full-Stack Verification
Coordination Checklist
Type Safety:
- No
anytypes at API boundaries - Request types match backend expectations
- Response types match frontend usage
- Error types handled consistently
Data Flow:
- Frontend sends correct request shape
- Backend validates and processes
- Backend returns correct response shape
- Frontend renders response correctly
Error Handling:
- Backend returns structured errors
- Frontend catches and displays errors
- Validation errors show field-level feedback
- Network errors handled gracefully
Run Full Verification
# Start backend
npm run dev:backend &
BACKEND_PID=$!
# Wait for backend
sleep 5
# Start frontend
npm run dev:frontend &
FRONTEND_PID=$!
# Wait for frontend
sleep 5
# Run E2E tests
npm run test:e2e
# Cleanup
kill $BACKEND_PID $FRONTEND_PID
Output Format
Full-Stack Report
# Full-Stack Integration Report
**Date**: [timestamp]
**Verdict**: [PASS | FAIL]
## Contract Verification
| Endpoint | Backend | Frontend | E2E |
|----------|---------|----------|-----|
| GET /users | PASS | PASS | PASS |
| POST /users | PASS | PASS | PASS |
| GET /users/:id | PASS | PASS | PASS |
## Type Safety
- Shared types: [Yes/No]
- Generated types: [Yes/No]
- No `any` at boundaries: [Yes/No]
## E2E Results
| Flow | Status | Details |
|------|--------|---------|
| User creation | PASS | [time] |
| User listing | PASS | [time] |
| Error handling | PASS | [time] |
## Issues Found
[List any integration issues]
## Recommendations
[Cross-stack improvements]
Integration with Deep Loop
When used within deep loop:
- Create contract first (PLAN phase output)
- Build backend, then frontend (BUILD phase)
- Run full-stack verification (REVIEW phase)
- Fix integration issues (FIX phase)
- Verify E2E tests pass (completion criteria)
