eovidiu

fullstack-dev

2
0
# Install this skill:
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

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.md for Server Component and Server Action patterns
  • See references/spa-patterns.md for React Router, TanStack Query, and SPA architecture patterns
  • See references/fastify-performance.md for backend optimization and performance tuning
  • See references/testing-strategy.md for behavior-driven testing (covers both server-first and SPA)
  • 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.