Build or update the BlueBubbles external channel plugin for Moltbot (extension package, REST...
npx skills add eovidiu/agents-skills --skill "fullstack-dev"
Install specific skill from multi-skill repository
# Description
Expert full-stack development skill for modern web applications covering both React 19 server-first architecture and traditional SPA + REST API patterns. Master Fastify backends, behavior-driven testing, and choose the right architecture for your needs. Use when building production-ready applications with Server Components, REST APIs, schema-based validation, and comprehensive testing strategies. Trigger phrases include "build a full-stack app", "create SPA with REST API", "setup Fastify API", "implement server actions", "write behavior tests", or any full-stack architecture questions.
# SKILL.md
name: fullstack-dev
description: Expert full-stack development skill for modern web applications covering both React 19 server-first architecture and traditional SPA + REST API patterns. Master Fastify backends, behavior-driven testing, and choose the right architecture for your needs. Use when building production-ready applications with Server Components, REST APIs, schema-based validation, and comprehensive testing strategies. Trigger phrases include "build a full-stack app", "create SPA with REST API", "setup Fastify API", "implement server actions", "write behavior tests", or any full-stack architecture questions.
Full-Stack Development Expert
Master full-stack web development with both modern server-first and traditional SPA architectures. Build production-ready applications using React 19 Server Components, traditional SPAs with REST APIs, high-performance Fastify backends, and comprehensive behavior-driven testing strategies.
Core Competencies
🖥️ Frontend: Dual Architecture Mastery
React 19 Server-First Architecture
Technologies: React 19, Vite 6, Tailwind CSS, shadcn/ui
Philosophy: Architect applications around React Server Components (RSC) and Server Actions for zero-bundle-size data fetching.
Key Capabilities:
- Design server-first component architectures
- Implement Server Actions with optimistic updates
- Build zero-bundle-size data-heavy interfaces
- Master Vite build toolchain optimization
- Create bespoke design systems with shadcn/ui
Best for:
- Content-heavy applications (dashboards, admin panels)
- SEO-critical pages
- Applications with heavy server-side data processing
- Minimal client-side JavaScript requirements
Traditional SPA + REST API Architecture
Technologies: React 18/19 (client-side), React Router, TanStack Query, Axios
Philosophy: Build interactive single-page applications with client-side routing and REST API communication for maximum interactivity.
Key Capabilities:
- Design scalable SPA architectures with React Router
- Implement client-side state management (React Context, Zustand)
- Master data fetching with TanStack Query (React Query)
- Handle authentication flows (JWT, OAuth)
- Build real-time features with WebSockets
- Optimize bundle splitting and lazy loading
Best for:
- Highly interactive applications (collaboration tools, real-time dashboards)
- Applications requiring offline-first capabilities
- Complex client-side state management needs
- Mobile-like user experiences
⚙️ Backend: Low-Overhead Service Engineering
Technologies: Fastify, SQLite
Philosophy: Engineer for maximum throughput and minimal overhead using schema-based validation and in-process databases.
Key Capabilities:
- Build high-performance Fastify services
- Implement schema-based request/response validation
- Design pragmatic SQLite data architectures
- Optimize for read-heavy workloads
- Master Fastify's plugin ecosystem
🧪 Testing: Comprehensive Testing Strategy
Technologies: Vitest, React Testing Library, MSW (Mock Service Worker), Jest
Philosophy: Write user-centric, behavior-driven tests that validate what users experience, not implementation details. Test both architectures appropriately.
Key Capabilities:
- Write behavior-driven frontend tests with RTL
- Test SPA routing and navigation flows
- Mock REST API calls with MSW
- Test authentication and authorization flows
- Implement isolated backend integration tests
- Use in-memory testing for fast feedback
- Mock external dependencies effectively
- Test Server Components and Server Actions
- E2E testing for critical user journeys
When to Use This Skill
Use this skill when:
Architecture & Design:
- Choosing between server-first and SPA architectures
- Building modern full-stack web applications
- Implementing React 19 Server Components architecture
- Creating traditional SPAs with REST APIs
- Designing scalable API architectures
Backend Development:
- Creating high-performance Fastify APIs
- Implementing RESTful API endpoints
- Setting up JWT authentication
- Designing database schemas with SQLite
Frontend Development:
- Building SPAs with React Router
- Implementing client-side state management
- Setting up TanStack Query for data fetching
- Optimizing build toolchains with Vite
- Designing component systems with Tailwind + shadcn/ui
- Implementing Server Actions with optimistic UI
Testing:
- Setting up behavior-driven testing strategies
- Testing SPA navigation and routing
- Mocking API calls in tests
- Writing E2E tests for user flows
Trigger phrases:
- "Build a full-stack app"
- "Create SPA with REST API"
- "Setup React Router with authentication"
- "Implement JWT authentication"
- "Create Server Component for data fetching"
- "Setup Fastify REST API"
- "Setup TanStack Query"
- "Implement Server Action with optimistic update"
- "Write behavior tests for SPA"
- "Mock API calls in tests"
- "Test authentication flow"
- "Optimize Vite build configuration"
- "Design shadcn/ui component system"
Architecture Patterns
Server-First Frontend Pattern
// app/dashboard/page.tsx - Server Component (zero bundle size)
import { getUserData, getStats } from '@/lib/data'
export default async function DashboardPage() {
// Data fetching happens on server
const user = await getUserData()
const stats = await getStats()
return (
<div className="container">
<h1>Welcome, {user.name}</h1>
{/* Server Component - no JS sent to client */}
<StatsGrid stats={stats} />
{/* Client Component - interactive parts only */}
<InteractiveChart data={stats.chartData} />
</div>
)
}
// components/stats-grid.tsx - Server Component
export function StatsGrid({ stats }: { stats: Stats }) {
return (
<div className="grid grid-cols-3 gap-4">
{stats.map(stat => (
<div key={stat.id} className="rounded-lg border p-4">
<p className="text-sm text-muted-foreground">{stat.label}</p>
<p className="text-2xl font-bold">{stat.value}</p>
</div>
))}
</div>
)
}
// components/interactive-chart.tsx - Client Component
'use client'
import { useState } from 'react'
export function InteractiveChart({ data }: { data: ChartData }) {
const [range, setRange] = useState('7d')
return (
<div>
<select value={range} onChange={e => setRange(e.target.value)}>
<option value="7d">Last 7 days</option>
<option value="30d">Last 30 days</option>
</select>
<Chart data={data} range={range} />
</div>
)
}
Server Actions with Optimistic Updates
// app/actions.ts - Server Actions
'use server'
import { revalidatePath } from 'next/cache'
import { db } from '@/lib/db'
export async function createTodo(formData: FormData) {
const title = formData.get('title') as string
// Server-side validation
if (!title || title.length < 3) {
return { error: 'Title must be at least 3 characters' }
}
// Database operation
const todo = await db.insert('todos').values({ title }).returning()
// Revalidate the page cache
revalidatePath('/todos')
return { todo }
}
// components/todo-form.tsx - Client Component with optimistic update
'use client'
import { useOptimistic } from 'react'
import { createTodo } from '@/app/actions'
export function TodoForm({ todos }: { todos: Todo[] }) {
const [optimisticTodos, addOptimisticTodo] = useOptimistic(
todos,
(state, newTodo: string) => [
...state,
{ id: crypto.randomUUID(), title: newTodo, completed: false }
]
)
async function handleSubmit(formData: FormData) {
const title = formData.get('title') as string
// Immediately update UI (optimistic)
addOptimisticTodo(title)
// Server action runs in background
await createTodo(formData)
}
return (
<div>
<form action={handleSubmit}>
<input name="title" placeholder="New todo..." required />
<button type="submit">Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
)
}
High-Performance Fastify Backend
// server/index.ts
import Fastify from 'fastify'
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'
import { Type } from '@sinclair/typebox'
const server = Fastify({
logger: true
}).withTypeProvider<TypeBoxTypeProvider>()
// Schema-based route with automatic validation and serialization
server.post('/api/users', {
schema: {
body: Type.Object({
name: Type.String({ minLength: 1 }),
email: Type.String({ format: 'email' })
}),
response: {
201: Type.Object({
id: Type.String(),
name: Type.String(),
email: Type.String()
})
}
}
}, async (request, reply) => {
const { name, email } = request.body
// Type-safe: body is automatically validated
const user = await db.insert('users').values({ name, email }).returning()
// Fast serialization: response schema pre-compiled
reply.code(201).send(user)
})
// Fastify plugin for database
import fp from 'fastify-plugin'
import Database from 'better-sqlite3'
export default fp(async (fastify) => {
const db = new Database('app.db')
// Optimize SQLite for read-heavy workloads
db.pragma('journal_mode = WAL')
db.pragma('synchronous = NORMAL')
db.pragma('cache_size = 10000')
fastify.decorate('db', db)
fastify.addHook('onClose', () => db.close())
})
server.listen({ port: 3000 })
Behavior-Driven Testing
// __tests__/todo-form.test.tsx - Frontend behavior test
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TodoForm } from '@/components/todo-form'
describe('TodoForm', () => {
it('allows users to add todos with optimistic updates', async () => {
const user = userEvent.setup()
render(<TodoForm todos={[]} />)
// Find form elements by accessible roles/labels
const input = screen.getByPlaceholderText(/new todo/i)
const button = screen.getByRole('button', { name: /add/i })
// User behavior: type and submit
await user.type(input, 'Buy milk')
await user.click(button)
// Assert on visible outcome
expect(screen.getByText('Buy milk')).toBeInTheDocument()
})
})
// __tests__/users.test.ts - Backend integration test
import { describe, it, expect, beforeEach } from 'vitest'
import { build } from '../server'
describe('POST /api/users', () => {
let server: FastifyInstance
beforeEach(async () => {
server = await build()
})
it('creates a user with valid data', async () => {
// Simulate HTTP request in-memory
const response = await server.inject({
method: 'POST',
url: '/api/users',
payload: {
name: 'John Doe',
email: '[email protected]'
}
})
// Assert on HTTP response
expect(response.statusCode).toBe(201)
expect(response.json()).toMatchObject({
name: 'John Doe',
email: '[email protected]'
})
})
it('rejects invalid email', async () => {
const response = await server.inject({
method: 'POST',
url: '/api/users',
payload: {
name: 'John',
email: 'invalid-email'
}
})
expect(response.statusCode).toBe(400)
})
})
Traditional SPA + REST API Pattern
Complete example of building a traditional single-page application with REST API backend.
SPA Frontend with React Router & TanStack Query
// src/main.tsx - Application entry point
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { App } from './App'
import './index.css'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
refetchOnWindowFocus: false,
},
},
})
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<BrowserRouter>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</BrowserRouter>
</React.StrictMode>
)
// src/App.tsx - Main app with routing
import { Routes, Route, Navigate } from 'react-router-dom'
import { useAuth } from './hooks/useAuth'
import { LoginPage } from './pages/LoginPage'
import { DashboardPage } from './pages/DashboardPage'
import { UsersPage } from './pages/UsersPage'
import { ProtectedRoute } from './components/ProtectedRoute'
export function App() {
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
<Route
path="/users"
element={
<ProtectedRoute>
<UsersPage />
</ProtectedRoute>
}
/>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
)
}
// src/hooks/useAuth.tsx - Authentication hook
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
import { api } from '../lib/api'
interface AuthState {
token: string | null
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
isAuthenticated: boolean
}
export const useAuth = create<AuthState>()(
persist(
(set, get) => ({
token: null,
user: null,
isAuthenticated: false,
login: async (email, password) => {
const response = await api.post('/auth/login', { email, password })
const { token, user } = response.data
set({ token, user, isAuthenticated: true })
// Set token for future requests
api.defaults.headers.common['Authorization'] = `Bearer ${token}`
},
logout: () => {
set({ token: null, user: null, isAuthenticated: false })
delete api.defaults.headers.common['Authorization']
},
}),
{
name: 'auth-storage',
}
)
)
// src/hooks/useUsers.ts - Data fetching with TanStack Query
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { api } from '../lib/api'
export function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const response = await api.get('/users')
return response.data
},
})
}
export function useCreateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (userData: CreateUserData) => {
const response = await api.post('/users', userData)
return response.data
},
onSuccess: () => {
// Invalidate and refetch users list
queryClient.invalidateQueries({ queryKey: ['users'] })
},
})
}
export function useUpdateUser() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ id, data }: { id: string; data: UpdateUserData }) => {
const response = await api.put(`/users/${id}`, data)
return response.data
},
onSuccess: (_, variables) => {
queryClient.invalidateQueries({ queryKey: ['users'] })
queryClient.invalidateQueries({ queryKey: ['users', variables.id] })
},
})
}
// src/pages/UsersPage.tsx - Component using TanStack Query
import { useUsers, useCreateUser } from '../hooks/useUsers'
import { UserForm } from '../components/UserForm'
import { UserList } from '../components/UserList'
export function UsersPage() {
const { data: users, isLoading, error } = useUsers()
const createUser = useCreateUser()
if (isLoading) return <div>Loading users...</div>
if (error) return <div>Error loading users: {error.message}</div>
return (
<div>
<h1>Users</h1>
<UserForm
onSubmit={(data) => {
createUser.mutate(data, {
onSuccess: () => {
// Show success notification
toast.success('User created!')
},
})
}}
isSubmitting={createUser.isPending}
/>
<UserList users={users} />
</div>
)
}
REST API Backend with Fastify
// server/routes/users.ts - RESTful API endpoints
import { FastifyPluginAsync } from 'fastify'
import { Type } from '@sinclair/typebox'
const usersRoutes: FastifyPluginAsync = async (server) => {
// GET /api/users - List all users
server.get('/users', {
onRequest: [server.authenticate], // Protected route
schema: {
response: {
200: Type.Array(UserSchema)
}
}
}, async (request) => {
const users = await server.db
.select()
.from('users')
.where('organizationId', request.user.organizationId)
return users
})
// POST /api/users - Create user
server.post('/users', {
onRequest: [server.authenticate],
schema: {
body: CreateUserSchema,
response: {
201: UserSchema
}
}
}, async (request, reply) => {
const [user] = await server.db
.insert('users')
.values({
...request.body,
organizationId: request.user.organizationId
})
.returning()
reply.code(201).send(user)
})
// GET /api/users/:id - Get single user
server.get('/users/:id', {
onRequest: [server.authenticate],
schema: {
params: Type.Object({
id: Type.String({ format: 'uuid' })
}),
response: {
200: UserSchema,
404: ErrorSchema
}
}
}, async (request, reply) => {
const user = await server.db
.select()
.from('users')
.where('id', request.params.id)
.first()
if (!user) {
reply.code(404).send({ error: 'User not found' })
return
}
return user
})
// PUT /api/users/:id - Update user
server.put('/users/:id', {
onRequest: [server.authenticate],
schema: {
params: Type.Object({
id: Type.String({ format: 'uuid' })
}),
body: UpdateUserSchema,
response: {
200: UserSchema
}
}
}, async (request, reply) => {
const [user] = await server.db
.update('users')
.set(request.body)
.where('id', request.params.id)
.returning()
if (!user) {
reply.code(404).send({ error: 'User not found' })
return
}
return user
})
// DELETE /api/users/:id - Delete user
server.delete('/users/:id', {
onRequest: [server.authenticate],
schema: {
params: Type.Object({
id: Type.String({ format: 'uuid' })
}),
response: {
204: Type.Null()
}
}
}, async (request, reply) => {
await server.db
.delete('users')
.where('id', request.params.id)
reply.code(204).send()
})
}
export default usersRoutes
// server/routes/auth.ts - JWT Authentication
import { FastifyPluginAsync } from 'fastify'
import { Type } from '@sinclair/typebox'
import bcrypt from 'bcryptjs'
const authRoutes: FastifyPluginAsync = async (server) => {
// POST /api/auth/login
server.post('/auth/login', {
schema: {
body: Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String({ minLength: 8 })
}),
response: {
200: Type.Object({
token: Type.String(),
user: UserSchema
}),
401: ErrorSchema
}
}
}, async (request, reply) => {
const { email, password } = request.body
const user = await server.db
.select()
.from('users')
.where('email', email)
.first()
if (!user || !await bcrypt.compare(password, user.passwordHash)) {
reply.code(401).send({
error: 'INVALID_CREDENTIALS',
message: 'Invalid email or password'
})
return
}
const token = server.jwt.sign({
userId: user.id,
email: user.email,
organizationId: user.organizationId
})
return {
token,
user: omit(user, 'passwordHash')
}
})
// POST /api/auth/register
server.post('/auth/register', {
schema: {
body: RegisterSchema,
response: {
201: Type.Object({
token: Type.String(),
user: UserSchema
})
}
}
}, async (request, reply) => {
const { email, password, name } = request.body
const passwordHash = await bcrypt.hash(password, 10)
const [user] = await server.db
.insert('users')
.values({ email, passwordHash, name })
.returning()
const token = server.jwt.sign({
userId: user.id,
email: user.email
})
reply.code(201).send({
token,
user: omit(user, 'passwordHash')
})
})
// GET /api/auth/me - Get current user
server.get('/auth/me', {
onRequest: [server.authenticate],
schema: {
response: {
200: UserSchema
}
}
}, async (request) => {
const user = await server.db
.select()
.from('users')
.where('id', request.user.userId)
.first()
return omit(user, 'passwordHash')
})
}
export default authRoutes
Testing SPA with MSW (Mock Service Worker)
// src/mocks/handlers.ts - API mocks for testing
import { http, HttpResponse } from 'msw'
export const handlers = [
// Mock GET /api/users
http.get('/api/users', () => {
return HttpResponse.json([
{ id: '1', name: 'John Doe', email: '[email protected]' },
{ id: '2', name: 'Jane Smith', email: '[email protected]' }
])
}),
// Mock POST /api/users
http.post('/api/users', async ({ request }) => {
const body = await request.json()
return HttpResponse.json(
{
id: crypto.randomUUID(),
...body,
createdAt: new Date().toISOString()
},
{ status: 201 }
)
}),
// Mock authentication
http.post('/api/auth/login', async ({ request }) => {
const { email, password } = await request.json()
if (email === '[email protected]' && password === 'password123') {
return HttpResponse.json({
token: 'mock-jwt-token',
user: { id: '1', email, name: 'Test User' }
})
}
return HttpResponse.json(
{ error: 'Invalid credentials' },
{ status: 401 }
)
})
]
// src/__tests__/UsersPage.test.tsx - Testing with MSW
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { BrowserRouter } from 'react-router-dom'
import { UsersPage } from '../pages/UsersPage'
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false }
}
})
return ({ children }) => (
<BrowserRouter>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</BrowserRouter>
)
}
describe('UsersPage', () => {
it('displays users from API', async () => {
render(<UsersPage />, { wrapper: createWrapper() })
// MSW intercepts request and returns mock data
expect(await screen.findByText('John Doe')).toBeInTheDocument()
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
})
it('allows creating new user', async () => {
const user = userEvent.setup()
render(<UsersPage />, { wrapper: createWrapper() })
// Wait for initial load
await screen.findByText('John Doe')
// Fill form
await user.type(screen.getByLabelText(/name/i), 'New User')
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.click(screen.getByRole('button', { name: /create/i }))
// New user appears in list (MSW returns mocked response)
expect(await screen.findByText('New User')).toBeInTheDocument()
})
})
// src/__tests__/auth.test.tsx - Testing authentication flow
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { BrowserRouter } from 'react-router-dom'
import { LoginPage } from '../pages/LoginPage'
describe('Authentication', () => {
it('logs in successfully with valid credentials', async () => {
const user = userEvent.setup()
render(<LoginPage />, { wrapper: BrowserRouter })
// Enter credentials
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.type(screen.getByLabelText(/password/i), 'password123')
await user.click(screen.getByRole('button', { name: /login/i }))
// MSW returns successful response
expect(await screen.findByText(/welcome/i)).toBeInTheDocument()
})
it('shows error with invalid credentials', async () => {
const user = userEvent.setup()
render(<LoginPage />, { wrapper: BrowserRouter })
await user.type(screen.getByLabelText(/email/i), '[email protected]')
await user.type(screen.getByLabelText(/password/i), 'wrongpassword')
await user.click(screen.getByRole('button', { name: /login/i }))
// MSW returns 401 error
expect(await screen.findByText(/invalid credentials/i)).toBeInTheDocument()
})
})
Project Structure
fullstack-app/
├── package.json # Root workspace config
├── vite.config.ts # Vite build configuration
├── tailwind.config.js # Tailwind + design tokens
├── app/ # React 19 app directory
│ ├── layout.tsx # Root layout (Server Component)
│ ├── page.tsx # Home page (Server Component)
│ ├── actions.ts # Server Actions
│ └── dashboard/
│ └── page.tsx # Dashboard (Server Component)
├── components/
│ ├── ui/ # shadcn/ui components (owned)
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ └── form.tsx
│ └── client/ # Client Components
│ └── interactive-chart.tsx
├── lib/
│ ├── db.ts # Database client
│ ├── data.ts # Data access layer
│ └── utils.ts # Shared utilities
├── server/ # Fastify backend
│ ├── index.ts # Server entry
│ ├── routes/ # API routes
│ │ ├── users.ts
│ │ └── todos.ts
│ ├── plugins/ # Fastify plugins
│ │ └── database.ts
│ └── schemas/ # Type schemas
│ └── user.ts
├── __tests__/
│ ├── components/ # Frontend tests (Vitest + RTL)
│ └── api/ # Backend tests (Vitest/Jest)
└── public/ # Static assets
Development Workflow
Initial Setup
# Create project
npm create vite@latest my-app -- --template react-ts
cd my-app
# Install frontend dependencies
npm install react@rc react-dom@rc
npm install -D tailwindcss postcss autoprefixer
npm install -D @testing-library/react @testing-library/user-event vitest
# Install backend dependencies
npm install fastify @fastify/type-provider-typebox @sinclair/typebox
npm install better-sqlite3
# Setup Tailwind
npx tailwindcss init -p
# Setup shadcn/ui
npx shadcn-ui@latest init
Development Commands
# Start development servers
npm run dev # Both frontend + backend
npm run dev:frontend # Vite dev server (5173)
npm run dev:backend # Fastify server (3000)
# Testing
npm run test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # Coverage report
# Build
npm run build # Production build
npm run preview # Preview production build
# Database
npm run db:migrate # Run migrations
npm run db:reset # Reset database
npm run db:seed # Seed test data
package.json Scripts
{
"scripts": {
"dev": "concurrently \"npm run dev:frontend\" \"npm run dev:backend\"",
"dev:frontend": "vite",
"dev:backend": "tsx watch server/index.ts",
"build": "vite build",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest --coverage",
"db:migrate": "tsx scripts/migrate.ts",
"db:reset": "tsx scripts/reset-db.ts",
"db:seed": "tsx scripts/seed.ts"
}
}
Best Practices
React 19 Server Components
DO:
- ✅ Use Server Components by default for data fetching
- ✅ Place 'use client' only where interactivity is needed
- ✅ Fetch data at the highest level possible
- ✅ Use Server Actions for mutations
- ✅ Combine Server Actions with useOptimistic for instant UI
DON'T:
- ❌ Don't fetch data in useEffect (use Server Components)
- ❌ Don't make every component a Client Component
- ❌ Don't use client-side state management for server data
- ❌ Don't build APIs just for your own frontend
Fastify Performance
DO:
- ✅ Define JSON schemas for all routes
- ✅ Use TypeBox for type-safe schemas
- ✅ Leverage schema-based serialization
- ✅ Use Fastify plugins for encapsulation
- ✅ Enable SQLite WAL mode for concurrency
DON'T:
- ❌ Don't skip schema validation (it's free performance)
- ❌ Don't use manual JSON.stringify (schemas are faster)
- ❌ Don't ignore Fastify's lifecycle hooks
- ❌ Don't put all routes in one file
Testing Strategy
DO:
- ✅ Test user behavior, not implementation
- ✅ Query by accessible roles and labels
- ✅ Use inject() for backend tests (no network)
- ✅ Mock database layer for speed
- ✅ Test happy path and error cases
DON'T:
- ❌ Don't test internal component state
- ❌ Don't query by CSS classes or test IDs
- ❌ Don't make real HTTP requests in tests
- ❌ Don't test library code (React, Fastify)
- ❌ Don't aim for 100% coverage (test what matters)
Common Patterns
Data Fetching in Server Components
// ✅ Good: Fetch in Server Component
export default async function UsersPage() {
const users = await db.select().from('users')
return <UserList users={users} />
}
// ❌ Bad: Client-side fetching
'use client'
export default function UsersPage() {
const [users, setUsers] = useState([])
useEffect(() => {
fetch('/api/users')
.then(r => r.json())
.then(setUsers)
}, [])
return <UserList users={users} />
}
Form Handling with Server Actions
// ✅ Good: Server Action with progressive enhancement
'use server'
export async function updateProfile(formData: FormData) {
const name = formData.get('name')
await db.update('users').set({ name })
revalidatePath('/profile')
}
// Component works without JavaScript!
export function ProfileForm({ user }) {
return (
<form action={updateProfile}>
<input name="name" defaultValue={user.name} />
<button>Save</button>
</form>
)
}
Fastify Route Organization
// routes/users.ts - Plugin-based route organization
import { FastifyPluginAsync } from 'fastify'
import { Type } from '@sinclair/typebox'
const usersRoutes: FastifyPluginAsync = async (server) => {
// All user routes in one plugin
server.get('/users', { schema: {...} }, getUsersHandler)
server.post('/users', { schema: {...} }, createUserHandler)
server.get('/users/:id', { schema: {...} }, getUserHandler)
}
export default usersRoutes
// server/index.ts
server.register(usersRoutes, { prefix: '/api' })
Troubleshooting
Frontend Issues
"ReferenceError: document is not defined"
- Cause: Using browser APIs in Server Component
- Fix: Move to Client Component with 'use client'
"Hydration mismatch"
- Cause: Server/client render different content
- Fix: Use useEffect for client-only content
Slow build times
- Check: Large dependencies in Server Components
- Fix: Use dynamic imports for heavy libraries
Backend Issues
"Port already in use"
# Kill process on port 3000
lsof -ti:3000 | xargs kill -9
"Database locked"
- Cause: SQLite not in WAL mode
- Fix: Add db.pragma('journal_mode = WAL')
Slow request validation
- Cause: Missing schema definitions
- Fix: Add TypeBox schemas to all routes
Testing Issues
"Cannot find module" in tests
- Fix: Add vitest aliases to vite.config.ts
Tests timing out
- Cause: Making real HTTP requests
- Fix: Use server.inject() instead
Related Skills
This skill works best in combination with other specialized skills from the marketplace:
Frontend & Testing
- frontend-reviewer-skill - Expert code review for React/Vue/Angular applications with accessibility and performance checks
- tdd-ui-expert - Pragmatic Test-Driven Development for React applications with behavioral testing focus
- web-accessibility-checker - WCAG 2.2 Level AA and EU EAA compliance checking for web applications
Backend & APIs
- fastify-expert - Advanced Fastify framework patterns, performance optimization, and production deployment strategies
- database-sqlite-ops - SQLite database management with migrations, seeding, and reset capabilities for interviewer-roster apps
DevOps & Quality
- github-manager - GitHub operations (PRs, issues, workflows) with safety checks and multi-account support
- skill-security-analyzer - Security analysis for Claude Code skills to detect vulnerabilities and malicious patterns
- skill-quality-analyzer - Comprehensive quality analysis across structure, security, UX, and code quality dimensions
Workflow Integration Examples
Full-Stack Development Flow:
1. fullstack-dev → Choose architecture (server-first vs SPA) & setup project
2. tdd-ui-expert → Write tests first using TDD approach
3. fullstack-dev → Implement features following chosen architecture
4. frontend-reviewer-skill → Code review for React best practices
5. web-accessibility-checker → Accessibility audit (WCAG 2.2)
6. github-manager → Create PR and merge with safety checks
Backend-Heavy Projects:
1. fullstack-dev → Setup Fastify + SQLite architecture
2. fastify-expert → Implement advanced API patterns and optimizations
3. database-sqlite-ops → Database migrations and seeding
4. fullstack-dev → Write comprehensive integration tests
5. skill-security-analyzer → Security audit before deployment
SPA Projects:
1. fullstack-dev → Setup SPA architecture (React Router + TanStack Query)
2. tdd-ui-expert → TDD workflow for components and hooks
3. frontend-reviewer-skill → Review SPA patterns and performance
4. web-accessibility-checker → Ensure WCAG compliance
5. github-manager → PR workflow and deployment
Quality Assurance Flow:
1. fullstack-dev → Build application
2. skill-security-analyzer → Security audit
3. skill-quality-analyzer → Comprehensive quality check
4. web-accessibility-checker → Accessibility compliance
5. github-manager → Ship to production
Resources
Documentation References
- See
references/react-19-patterns.mdfor Server Component and Server Action patterns - See
references/spa-patterns.mdfor React Router, TanStack Query, and SPA architecture patterns - See
references/fastify-performance.mdfor backend optimization and performance tuning - See
references/testing-strategy.mdfor behavior-driven testing (covers both server-first and SPA)
Quick Links
- React 19 Docs: https://react.dev
- React Router: https://reactrouter.com
- TanStack Query: https://tanstack.com/query
- Fastify Docs: https://fastify.dev
- Vite Guide: https://vitejs.dev
- Vitest Docs: https://vitest.dev
- shadcn/ui: https://ui.shadcn.com
- TypeBox: https://github.com/sinclairzx81/typebox
- MSW (Mock Service Worker): https://mswjs.io
Philosophy: Build server-first, optimize pragmatically, test behaviors. Ship fast, perform faster.
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.