fullstack-modern

Modern Fullstack Integration Patterns

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "fullstack-modern" with this command: npx skills add twofoldtech-dakota/claude-marketplace/twofoldtech-dakota-claude-marketplace-fullstack-modern

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 */}
  &#x3C;ProductFilters initialCategory={searchParams.category} />
  
  {/* Server-rendered product grid */}
  &#x3C;ProductGrid products={products.items} />
  
  {/* Client component for pagination */}
  &#x3C;Pagination
    currentPage={page}
    totalPages={products.totalPages}
  />
&#x3C;/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; }

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-razor

No summary provided by upstream source.

Repository SourceNeeds Review
General

optimizely-content-cloud

No summary provided by upstream source.

Repository SourceNeeds Review
General

optimizely-experimentation

No summary provided by upstream source.

Repository SourceNeeds Review
General

optimizely-web

No summary provided by upstream source.

Repository SourceNeeds Review