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
GitHub Stars
1
First Seen
Mar 1, 2026
Installed on
opencode3
gemini-cli3
github-copilot3
codex3
amp3
cline3