fullstack-modern
Apply when working with modern fullstack patterns including React/Vue, GraphQL, REST APIs, and headless architectures
Best use case
fullstack-modern is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Apply when working with modern fullstack patterns including React/Vue, GraphQL, REST APIs, and headless architectures
Teams using fullstack-modern should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/fullstack-modern/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How fullstack-modern Compares
| Feature / Agent | fullstack-modern | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Apply when working with modern fullstack patterns including React/Vue, GraphQL, REST APIs, and headless architectures
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Modern Fullstack Integration Patterns
## Data Flow Architecture
### Overview
```
┌─────────────────────────────────────────────────────────────┐
│ Client │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ React/ │───▶│ State │───▶│ UI │ │
│ │ Vue │ │ (Zustand/ │ │ Render │ │
│ │ Components│ │ Pinia) │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Data Fetching (React Query / SWR / Apollo) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼ HTTP / GraphQL
┌─────────────────────────────────────────────────────────────┐
│ Server │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ API │───▶│ Business │───▶│ Data │ │
│ │ Routes │ │ Logic │ │ Layer │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Next.js API Routes
### Basic API Route
```typescript
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { productService } from '@/lib/services/product-service';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const page = parseInt(searchParams.get('page') ?? '1');
const limit = parseInt(searchParams.get('limit') ?? '10');
const category = searchParams.get('category');
try {
const products = await productService.getProducts({
page,
limit,
category: category ?? undefined,
});
return NextResponse.json(products);
} catch (error) {
console.error('Failed to fetch products:', error);
return NextResponse.json(
{ error: 'Failed to fetch products' },
{ status: 500 }
);
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
// Validate request body
const validationResult = validateCreateProduct(body);
if (!validationResult.success) {
return NextResponse.json(
{ errors: validationResult.errors },
{ status: 400 }
);
}
const product = await productService.createProduct(body);
return NextResponse.json(product, { status: 201 });
} catch (error) {
console.error('Failed to create product:', error);
return NextResponse.json(
{ error: 'Failed to create product' },
{ status: 500 }
);
}
}
```
### Dynamic API Route
```typescript
// app/api/products/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
interface RouteParams {
params: { id: string };
}
export async function GET(request: NextRequest, { params }: RouteParams) {
const product = await productService.getById(params.id);
if (!product) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
return NextResponse.json(product);
}
export async function PUT(request: NextRequest, { params }: RouteParams) {
const body = await request.json();
try {
const product = await productService.update(params.id, body);
return NextResponse.json(product);
} catch (error) {
if (error instanceof NotFoundError) {
return NextResponse.json(
{ error: 'Product not found' },
{ status: 404 }
);
}
throw error;
}
}
export async function DELETE(request: NextRequest, { params }: RouteParams) {
await productService.delete(params.id);
return new NextResponse(null, { status: 204 });
}
```
## GraphQL Integration
### Query Definitions
```graphql
# queries/products.graphql
query GetProducts($first: Int!, $after: String, $category: String) {
products(first: $first, after: $after, category: $category) {
edges {
node {
id
name
slug
price
image {
url
alt
}
category {
name
}
}
cursor
}
pageInfo {
hasNextPage
endCursor
}
totalCount
}
}
query GetProductBySlug($slug: String!) {
product(slug: $slug) {
id
name
slug
description
price
images {
url
alt
}
category {
name
slug
}
variants {
id
name
sku
price
}
}
}
mutation AddToCart($productId: ID!, $quantity: Int!) {
addToCart(input: { productId: $productId, quantity: $quantity }) {
cart {
id
items {
product {
name
}
quantity
}
total
}
}
}
```
### GraphQL Client Setup
```typescript
// lib/graphql/client.ts
import { GraphQLClient } from 'graphql-request';
const endpoint = process.env.GRAPHQL_ENDPOINT!;
const apiKey = process.env.API_KEY!;
export const graphqlClient = new GraphQLClient(endpoint, {
headers: {
'x-api-key': apiKey,
},
});
// With error handling wrapper
export async function fetchGraphQL<T>(
query: string,
variables?: Record<string, unknown>
): Promise<T> {
try {
return await graphqlClient.request<T>(query, variables);
} catch (error) {
console.error('GraphQL Error:', error);
throw new Error('Failed to fetch data');
}
}
```
### React Hook for GraphQL
```typescript
// hooks/useProducts.ts
import { useQuery } from '@tanstack/react-query';
import { graphqlClient } from '@/lib/graphql/client';
import { GetProductsDocument } from '@/generated/graphql';
interface UseProductsOptions {
category?: string;
limit?: number;
}
export function useProducts({ category, limit = 10 }: UseProductsOptions = {}) {
return useQuery({
queryKey: ['products', { category, limit }],
queryFn: async () => {
const data = await graphqlClient.request(GetProductsDocument, {
first: limit,
category,
});
return data.products;
},
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
```
## Server Components + Client Data
### Server Component with Data
```tsx
// app/products/page.tsx
import { getProducts } from '@/lib/api/products';
import { ProductGrid } from '@/components/ProductGrid';
import { ProductFilters } from '@/components/ProductFilters';
interface PageProps {
searchParams: { category?: string; page?: string };
}
export default async function ProductsPage({ searchParams }: PageProps) {
const page = parseInt(searchParams.page ?? '1');
const products = await getProducts({
category: searchParams.category,
page,
limit: 12,
});
return (
<main className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-6">Products</h1>
{/* Client component for interactivity */}
<ProductFilters initialCategory={searchParams.category} />
{/* Server-rendered product grid */}
<ProductGrid products={products.items} />
{/* Client component for pagination */}
<Pagination
currentPage={page}
totalPages={products.totalPages}
/>
</main>
);
}
```
### Client Component with Mutations
```tsx
'use client';
import { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
interface AddToCartButtonProps {
productId: string;
productName: string;
}
export function AddToCartButton({ productId, productName }: AddToCartButtonProps) {
const [quantity, setQuantity] = useState(1);
const queryClient = useQueryClient();
const mutation = useMutation({
mutationFn: async (data: { productId: string; quantity: number }) => {
const response = await fetch('/api/cart', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error('Failed to add to cart');
}
return response.json();
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cart'] });
},
});
return (
<div className="flex items-center gap-4">
<input
type="number"
min="1"
max="10"
value={quantity}
onChange={(e) => setQuantity(parseInt(e.target.value))}
className="w-20 px-3 py-2 border rounded"
/>
<button
onClick={() => mutation.mutate({ productId, quantity })}
disabled={mutation.isPending}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{mutation.isPending ? 'Adding...' : 'Add to Cart'}
</button>
{mutation.isError && (
<span className="text-red-500">Failed to add item</span>
)}
</div>
);
}
```
## REST API Patterns
### API Client
```typescript
// lib/api/client.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
interface RequestOptions extends RequestInit {
params?: Record<string, string>;
}
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async request<T>(
endpoint: string,
options: RequestOptions = {}
): Promise<T> {
const { params, ...init } = options;
let url = `${this.baseUrl}${endpoint}`;
if (params) {
const searchParams = new URLSearchParams(params);
url += `?${searchParams.toString()}`;
}
const response = await fetch(url, {
...init,
headers: {
'Content-Type': 'application/json',
...init.headers,
},
});
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new ApiError(response.status, error.message ?? 'Request failed');
}
return response.json();
}
async get<T>(endpoint: string, params?: Record<string, string>): Promise<T> {
return this.request<T>(endpoint, { method: 'GET', params });
}
async post<T>(endpoint: string, data: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(data),
});
}
async put<T>(endpoint: string, data: unknown): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(data),
});
}
async delete(endpoint: string): Promise<void> {
await this.request(endpoint, { method: 'DELETE' });
}
}
export const api = new ApiClient(API_BASE_URL);
```
### Type-Safe API Hooks
```typescript
// hooks/api/useProducts.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api/client';
import type { Product, CreateProductInput, UpdateProductInput } from '@/types';
export function useProducts(params?: { category?: string; page?: number }) {
return useQuery({
queryKey: ['products', params],
queryFn: () => api.get<{ items: Product[]; total: number }>('/products', {
category: params?.category,
page: String(params?.page ?? 1),
}),
});
}
export function useProduct(id: string) {
return useQuery({
queryKey: ['products', id],
queryFn: () => api.get<Product>(`/products/${id}`),
enabled: !!id,
});
}
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: CreateProductInput) =>
api.post<Product>('/products', data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateProductInput }) =>
api.put<Product>(`/products/${id}`, data),
onSuccess: (product) => {
queryClient.invalidateQueries({ queryKey: ['products'] });
queryClient.setQueryData(['products', product.id], product);
},
});
}
export function useDeleteProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => api.delete(`/products/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
```
## Environment Variables
### Configuration
```bash
# .env.local
# Server-only (not exposed to browser)
DATABASE_URL=postgresql://user:pass@localhost:5432/db
API_SECRET_KEY=sk_live_xxxxx
GRAPHQL_ENDPOINT=https://api.example.com/graphql
# Public (exposed to browser via NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_ANALYTICS_ID=UA-XXXXX
```
### Type-Safe Environment
```typescript
// lib/env.ts
import { z } from 'zod';
const envSchema = z.object({
// Server-only
DATABASE_URL: z.string().url(),
API_SECRET_KEY: z.string().min(1),
GRAPHQL_ENDPOINT: z.string().url(),
// Public
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_SITE_URL: z.string().url(),
});
// Validate at build time
const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
console.error('Invalid environment variables:', parsed.error.flatten());
throw new Error('Invalid environment variables');
}
export const env = parsed.data;
// For client-side only variables
export const publicEnv = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
siteUrl: process.env.NEXT_PUBLIC_SITE_URL!,
};
```
## Error Handling
### Error Boundaries
```tsx
'use client';
import { useEffect } from 'react';
interface ErrorBoundaryProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) {
useEffect(() => {
// Log to error tracking service
console.error('Error:', error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-[400px] p-8">
<h2 className="text-2xl font-bold text-red-600 mb-4">
Something went wrong
</h2>
<p className="text-gray-600 mb-6">
{error.message || 'An unexpected error occurred'}
</p>
<button
onClick={reset}
className="px-6 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Try again
</button>
</div>
);
}
```
### API Error Handling
```typescript
// lib/errors.ts
export class ApiError extends Error {
constructor(
public status: number,
message: string,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
static isApiError(error: unknown): error is ApiError {
return error instanceof ApiError;
}
}
// Usage in components
function ProductPage() {
const { data, error, isLoading } = useProduct(id);
if (isLoading) return <Skeleton />;
if (error) {
if (ApiError.isApiError(error) && error.status === 404) {
return <NotFound message="Product not found" />;
}
return <ErrorMessage error={error} />;
}
return <ProductDetails product={data} />;
}
```
## Optimistic Updates
```typescript
// hooks/useToggleFavorite.ts
import { useMutation, useQueryClient } from '@tanstack/react-query';
export function useToggleFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (productId: string) => {
const response = await fetch(`/api/favorites/${productId}`, {
method: 'POST',
});
return response.json();
},
// Optimistic update
onMutate: async (productId) => {
// Cancel outgoing refetches
await queryClient.cancelQueries({ queryKey: ['products'] });
// Snapshot previous value
const previousProducts = queryClient.getQueryData(['products']);
// Optimistically update
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((product) =>
product.id === productId
? { ...product, isFavorite: !product.isFavorite }
: product
)
);
return { previousProducts };
},
// Rollback on error
onError: (err, productId, context) => {
queryClient.setQueryData(['products'], context?.previousProducts);
},
// Refetch after success or error
onSettled: () => {
queryClient.invalidateQueries({ queryKey: ['products'] });
},
});
}
```
## Real-Time Updates
### WebSocket Integration
```typescript
// lib/websocket.ts
import { useEffect, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export function useWebSocket(url: string) {
const ws = useRef<WebSocket | null>(null);
const queryClient = useQueryClient();
useEffect(() => {
ws.current = new WebSocket(url);
ws.current.onmessage = (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'product:updated':
queryClient.invalidateQueries({
queryKey: ['products', message.productId]
});
break;
case 'cart:updated':
queryClient.invalidateQueries({ queryKey: ['cart'] });
break;
case 'notification':
// Handle notification
break;
}
};
ws.current.onclose = () => {
// Reconnect logic
setTimeout(() => {
ws.current = new WebSocket(url);
}, 3000);
};
return () => {
ws.current?.close();
};
}, [url, queryClient]);
return {
send: (data: unknown) => {
ws.current?.send(JSON.stringify(data));
},
};
}
```
## SSR vs SSG vs ISR
### Static Generation (SSG)
```typescript
// For content that rarely changes
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug);
return <Article post={post} />;
}
```
### Incremental Static Regeneration (ISR)
```typescript
// For content that updates periodically
// app/products/page.tsx
export const revalidate = 60; // Revalidate every 60 seconds
export default async function ProductsPage() {
const products = await getProducts();
return <ProductGrid products={products} />;
}
```
### Server-Side Rendering (SSR)
```typescript
// For personalized or real-time data
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic';
export default async function DashboardPage() {
const user = await getCurrentUser();
const data = await getDashboardData(user.id);
return <Dashboard user={user} data={data} />;
}
```
## Preview Mode / Draft Content
```typescript
// app/api/preview/route.ts
import { draftMode } from 'next/headers';
import { redirect } from 'next/navigation';
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const secret = searchParams.get('secret');
const slug = searchParams.get('slug');
if (secret !== process.env.PREVIEW_SECRET) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
// Enable draft mode
draftMode().enable();
// Redirect to the page
redirect(slug ?? '/');
}
// app/api/exit-preview/route.ts
export async function GET() {
draftMode().disable();
redirect('/');
}
```
```typescript
// lib/api/content.ts
import { draftMode } from 'next/headers';
export async function getContent(slug: string) {
const { isEnabled: isDraft } = draftMode();
// Fetch draft or published content based on mode
const content = await cms.getContent(slug, {
preview: isDraft,
});
return content;
}
```Related Skills
fullstack-classic
Apply when working with classic fullstack patterns including jQuery AJAX, form handling, and C# MVC integration
dioxus-modern
Modern Dioxus 0.7 + Tailwind CSS development guide. Use when asking about Signals, Stores, ReadSignal props, async patterns, components, or Tailwind integration in Dioxus 0.7.
angular-modernization
Modernizes Angular code such as components and directives to follow best practices using both automatic CLI migrations and Bitwarden-specific patterns. YOU must use this skill when someone requests modernizing Angular code. DO NOT invoke for general Angular discussions unrelated to modernization.
senior-fullstack
Fullstack development toolkit with project scaffolding for Next.js/FastAPI/MERN/Django stacks and code quality analysis. Use when scaffolding new projects, analyzing codebase quality, or implementing fullstack architecture patterns.
senior-fullstack-ai-engineer
Senior full-stack developer with 10+ years of experience and AI engineering expertise. Builds production-ready applications using modern frameworks (Flask, FastAPI, React), AI/ML technologies (LLMs, RAG, model deployment), and cloud infrastructure. Use for all development tasks requiring full-stack and AI/ML implementation.
react-modernization
Upgrade React applications to latest versions, migrate from class components to hooks, and adopt concurrent features. Use when modernizing React codebases, migrating to React Hooks, or upgrading to...
modern-web-creator
Creates distinctive, human-quality websites using 2025 design philosophy—anti-design aesthetics, bold minimalism, organic shapes, and intentional imperfection. Specializes in React/TypeScript with Tailwind CSS, shadcn/ui, and custom micro-interactions. Prevents generic AI templates through specific constraints, asymmetric layouts, and brand-aligned creative direction. Use for portfolios, marketing sites, SaaS interfaces, or any project requiring unique visual identity beyond cookie-cutter designs.
modern-python-standards
Strict adherence to modern (3.11+), idiomatic, and type-safe Python development.
modern-python
Modern Python tooling best practices using uv, ruff, ty, and pytest. Mandates the Trail of Bits Python coding standards for project setup, dependency management, linting, type checking, and testing. Based on patterns from trailofbits/cookiecutter-python.
modern-javascript-patterns
Master ES6+ features including async/await, destructuring, spread operators, arrow functions, promises, modules, iterators, generators, and functional programming patterns for writing clean, effici...
modern-java-backend-playbook
Enforces backend Java/Quarkus project standards including architecture layers, design patterns, code reuse, Lombok, TDD, exception handling, and modern Java features. Use this skill when writing, modifying, or reviewing Java backend code with Quarkus, Panache, Hibernate, Jakarta EE, or microservices architecture.
legacy-modernizer
Refactor legacy codebases, migrate outdated frameworks, and implement gradual modernization. Handles technical debt, dependency updates, and backward compatibility.