| name | fullstack-modern |
| description | Apply when working with modern fullstack patterns including React/Vue, GraphQL, REST APIs, and headless architectures |
| globs | ["**/*.tsx","**/*.jsx","**/*.vue","**/api/**/*.ts","**/*.graphql","**/next.config.*"] |
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
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();
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
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 .(product);
} (error) {
(error ) {
.(
{ : },
{ : }
);
}
error;
}
}
() {
productService.(params.);
(, { : });
}
GraphQL Integration
Query Definitions
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
}
AddToCart: ID, : Int
addToCart ,
cart
id
items
product
name
quantity
total
GraphQL Client Setup
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,
},
});
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
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,
});
}
Server Components + Client Data
Server Component with Data
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 */}
{/* Client component for pagination */}
);
}
Client Component with Mutations
'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');
}
response.();
},
: {
queryClient.({ : [] });
},
});
(
);
}
REST API Patterns
API Client
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,
},
});
(!response.) {
error = response.().( ({}));
(response., error. ?? );
}
response.();
}
get<T>(: , ?: <, >): <T> {
.<T>(endpoint, { : , params });
}
post<T>(: , : ): <T> {
.<T>(endpoint, {
: ,
: .(data),
});
}
put<T>(: , : ): <T> {
.<T>(endpoint, {
: ,
: .(data),
});
}
(: ): <> {
.(endpoint, { : });
}
}
api = ();
Type-Safe API Hooks
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<>(),
: !!id,
});
}
() {
queryClient = ();
({
:
api.<>(, data),
: {
queryClient.({ : [] });
},
});
}
() {
queryClient = ();
({
:
api.<>(, data),
: {
queryClient.({ : [] });
queryClient.([, product.], product);
},
});
}
() {
queryClient = ();
({
: api.(),
: {
queryClient.({ : [] });
},
});
}
Environment Variables
Configuration
DATABASE_URL=postgresql://user:pass@localhost:5432/db
API_SECRET_KEY=sk_live_xxxxx
GRAPHQL_ENDPOINT=https://api.example.com/graphql
NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_ANALYTICS_ID=UA-XXXXX
Type-Safe Environment
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
API_SECRET_KEY: z.string().min(1),
GRAPHQL_ENDPOINT: z.string().url(),
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_SITE_URL: z.string().url(),
});
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;
export const publicEnv = {
apiUrl: process.env.NEXT_PUBLIC_API_URL!,
siteUrl: process..!,
};
Error Handling
Error Boundaries
'use client';
import { useEffect } from 'react';
interface ErrorBoundaryProps {
error: Error & { digest?: string };
reset: () => void;
}
export default function ErrorBoundary({ error, reset }: ErrorBoundaryProps) {
useEffect(() => {
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>
</>
);
}
API Error Handling
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;
}
}
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 < = />;
}
;
}
Optimistic Updates
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();
},
onMutate: async (productId) => {
await queryClient.cancelQueries({ queryKey: ['products'] });
const previousProducts = queryClient.getQueryData(['products']);
queryClient.setQueryData(['products'], (old: Product[]) =>
old.map((product) =>
product.id === productId
? { ...product, : !product. }
: product
)
);
{ previousProducts };
},
: {
queryClient.([], context?.);
},
: {
queryClient.({ : [] });
},
});
}
Real-Time Updates
WebSocket Integration
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':
;
}
};
ws.. = {
( {
ws. = (url);
}, );
};
{
ws.?.();
};
}, [url, queryClient]);
{
: {
ws.?.(.(data));
},
};
}
SSR vs SSG vs ISR
Static Generation (SSG)
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)
export const revalidate = 60;
export default async function ProductsPage() {
const products = await getProducts();
return <ProductGrid products={products} />;
}
Server-Side Rendering (SSR)
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
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 });
}
draftMode().enable();
redirect(slug ?? '/');
}
export async function GET() {
draftMode().disable();
redirect('/');
}
import { draftMode } from 'next/headers';
export async function getContent(slug: string) {
const { isEnabled: isDraft } = draftMode();
const content = await cms.getContent(slug, {
preview: isDraft,
});
return content;
}