fullstack-modern
SKILL.md
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
// 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
// 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
# 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
// 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
// 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
// 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
'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
// 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
// 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
# .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
// 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
'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
// 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
// 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
// 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)
// 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)
// 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)
// 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
// 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('/');
}
// 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;
}
Weekly Installs
3
Repository
twofoldtech-dak…ketplaceGitHub Stars
1
First Seen
Mar 1, 2026
Security Audits
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3