Back to Blog

Pricing Frontend: Next.js 14 Architecture Deep Dive

Comprehensive technical documentation of production codebase architectures

ArchitectureDocumentationTypeScriptC# .NETNext.jsNode.js
šŸ“š Comprehensive architecture guides

Pricing Frontend: Next.js 14 Architecture Deep Dive

Executive Summary

The pricing-frontend is a modern, serverless web application built with Next.js 14, React 18, and TypeScript that runs on Node.js runtime and is deployed using the SST (Serverless Stack) framework. This document provides a comprehensive technical explanation of how Next.js, React, and TypeScript are used throughout the architecture, from server-side rendering to real-time GraphQL data fetching and role-based access control.


Table of Contents

  1. Next.js 14 and React 18 Foundation
  2. TypeScript in Next.js
  3. Server-Side Rendering (SSR) Architecture
  4. State Management with Jotai and React Query
  5. GraphQL Integration
  6. Material-UI Component Library
  7. Authentication and Authorization
  8. SST Serverless Deployment
  9. Code Structure and Patterns
  10. Performance Optimization
  11. Development Workflow

Next.js 14 and React 18 Foundation

What is Next.js 14 in This Context?

Next.js 14 is a React framework built on Node.js that provides:

  • Server-Side Rendering (SSR) for fast initial page loads
  • App Router for modern routing and layouts
  • API Routes for backend functionality
  • Automatic code splitting for optimal performance
  • Built-in optimizations for images, fonts, and scripts
  • Serverless deployment support via SST

Why Next.js 14 for Pricing Frontend?

  1. Server-Side Rendering: Fast initial page loads for pricing staff
  2. Real-Time Updates: React 18's concurrent features enable smooth UI updates
  3. TypeScript Native: First-class TypeScript support
  4. Serverless Ready: SST framework integrates seamlessly with Next.js
  5. Performance: Optimized for fast loading times
  6. Business Impact: Enabled real-time franchisee pricing for 8,000+ locations

Runtime Environment

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   AWS Lambda Execution Environment  │
│   (via SST Framework)               │
│                                     │
│   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”   │
│   │   Node.js Runtime (18.x)     │   │
│   │                             │   │
│   │   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │   │
│   │   │ Next.js 14 Framework  │ │   │
│   │   │                       │ │   │
│   │   │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ │   │
│   │   │ │ React 18          │ │ │   │
│   │   │ │ (Compiled JS)     │ │ │   │
│   │   │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ │   │
│   │   │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ │   │
│   │   │ │ TypeScript        │ │ │   │
│   │   │ │ (Compiled to JS)  │ │ │   │
│   │   │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ │   │
│   │   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │   │
│   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

TypeScript in Next.js

TypeScript Configuration

The pricing-frontend uses TypeScript throughout for type safety:

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

TypeScript Component Example

// app/pricing/page.tsx
import { Metadata } from 'next';
import { PricingDashboard } from '@/components/pricing/PricingDashboard';
import { getPricingData } from '@/lib/graphql/queries';

export const metadata: Metadata = {
  title: 'Pricing Management',
  description: 'Manage pricing for all store locations',
};

interface PricingPageProps {
  searchParams: {
    storeId?: string;
    page?: string;
  };
}

export default async function PricingPage({ searchParams }: PricingPageProps) {
  // Server-side data fetching with TypeScript
  const storeId = searchParams.storeId;
  const page = parseInt(searchParams.page || '1', 10);
  
  const pricingData = await getPricingData({
    storeId,
    page,
    limit: 50,
  });
  
  return (
    <PricingDashboard
      initialData={pricingData}
      storeId={storeId}
    />
  );
}

Server-Side Rendering (SSR) Architecture

Next.js App Router Structure

The pricing-frontend uses Next.js 14 App Router for modern routing:

app/
ā”œā”€ā”€ layout.tsx              # Root layout (SSR)
ā”œā”€ā”€ page.tsx                 # Home page
ā”œā”€ā”€ pricing/
│   ā”œā”€ā”€ layout.tsx          # Pricing section layout
│   ā”œā”€ā”€ page.tsx            # Pricing dashboard (SSR)
│   ā”œā”€ā”€ [storeId]/
│   │   └── page.tsx        # Store-specific pricing (SSR)
│   └── reports/
│       └── page.tsx        # Pricing reports (SSR)
ā”œā”€ā”€ api/
│   └── auth/
│       └── [...nextauth]/
│           └── route.ts   # NextAuth API route
└── globals.css

Server Component Example

// app/pricing/page.tsx
import { Suspense } from 'react';
import { PricingTable } from '@/components/pricing/PricingTable';
import { PricingFilters } from '@/components/pricing/PricingFilters';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';

// This is a Server Component (runs on Node.js)
export default async function PricingPage() {
  // Server-side authentication check
  const session = await getServerSession(authOptions);
  
  if (!session) {
    redirect('/login');
  }
  
  // Server-side data fetching
  const initialPricingData = await fetchPricingData(session.user.storeIds);
  
  return (
    <div className="container mx-auto p-6">
      <h1 className="text-2xl font-bold mb-4">Pricing Management</h1>
      
      <Suspense fallback={<PricingFiltersSkeleton />}>
        <PricingFilters />
      </Suspense>
      
      <Suspense fallback={<PricingTableSkeleton />}>
        <PricingTable initialData={initialPricingData} />
      </Suspense>
    </div>
  );
}

SSR Data Fetching

// lib/graphql/server-queries.ts
import { getServerSession } from 'next-auth';
import { graphqlClient } from './client';

export async function fetchPricingData(storeIds: string[]) {
  const session = await getServerSession(authOptions);
  
  // Server-side GraphQL query
  const query = `
    query GetPricingData($storeIds: [ID!]!) {
      prices(storeIds: $storeIds) {
        id
        storeId
        productId
        amount
        currency
        effectiveDate
        expirationDate
      }
    }
  `;
  
  const response = await graphqlClient.request(query, {
    storeIds,
    headers: {
      Authorization: `Bearer ${session?.accessToken}`,
    },
  });
  
  return response.prices;
}

SSR Execution Flow

1. User requests /pricing
   ↓
2. Next.js server (Node.js) receives request
   ↓
3. Server Component executes
   - Runs getServerSession() (server-side)
   - Runs fetchPricingData() (server-side)
   - Renders React components to HTML
   ↓
4. HTML sent to client (with data)
   ↓
5. Client hydrates React components
   ↓
6. Interactive features enabled

State Management with Jotai and React Query

Jotai for UI State

Jotai is used for lightweight, atomic state management:

// atoms/pricing-atoms.ts
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';

// UI state atoms
export const selectedStoreIdAtom = atom<string | null>(null);
export const filterAtom = atom({
  search: '',
  category: 'all',
  dateRange: '30days',
});

// Persisted state (localStorage)
export const userPreferencesAtom = atomWithStorage('pricing-preferences', {
  pageSize: 50,
  defaultView: 'table',
  showInactive: false,
});

// Derived atoms
export const filteredPricesAtom = atom((get) => {
  const prices = get(pricesAtom);
  const filter = get(filterAtom);
  
  return prices.filter(price => {
    if (filter.search && !price.productName.includes(filter.search)) {
      return false;
    }
    if (filter.category !== 'all' && price.category !== filter.category) {
      return false;
    }
    return true;
  });
});

React Query for Server State

React Query manages server state, caching, and synchronization:

// hooks/use-pricing-data.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { graphqlClient } from '@/lib/graphql/client';

export function usePricingData(storeId: string | null) {
  return useQuery({
    queryKey: ['pricing', storeId],
    queryFn: async () => {
      if (!storeId) return null;
      
      const query = `
        query GetPricing($storeId: ID!) {
          prices(storeId: $storeId) {
            id
            storeId
            productId
            amount
            currency
            effectiveDate
          }
        }
      `;
      
      const response = await graphqlClient.request(query, { storeId });
      return response.prices;
    },
    enabled: !!storeId,
    staleTime: 30000, // 30 seconds
    refetchOnWindowFocus: true,
  });
}

export function useUpdatePrice() {
  const queryClient = useQueryClient();
  
  return useMutation({
    mutationFn: async (input: UpdatePriceInput) => {
      const mutation = `
        mutation UpdatePrice($input: UpdatePriceInput!) {
          updatePrice(input: $input) {
            id
            amount
            effectiveDate
          }
        }
      `;
      
      return await graphqlClient.request(mutation, { input });
    },
    onSuccess: (data, variables) => {
      // Invalidate and refetch pricing data
      queryClient.invalidateQueries({ queryKey: ['pricing', variables.storeId] });
    },
  });
}

Combined State Management

// components/pricing/PricingDashboard.tsx
'use client';

import { useAtom } from 'jotai';
import { usePricingData, useUpdatePrice } from '@/hooks/use-pricing-data';
import { selectedStoreIdAtom, filterAtom } from '@/atoms/pricing-atoms';

export function PricingDashboard() {
  const [selectedStoreId] = useAtom(selectedStoreIdAtom);
  const [filter, setFilter] = useAtom(filterAtom);
  
  // React Query for server state
  const { data: pricingData, isLoading, error } = usePricingData(selectedStoreId);
  const updatePriceMutation = useUpdatePrice();
  
  const handlePriceUpdate = async (priceId: string, newAmount: number) => {
    await updatePriceMutation.mutateAsync({
      id: priceId,
      amount: newAmount,
      storeId: selectedStoreId!,
    });
  };
  
  if (isLoading) return <LoadingSpinner />;
  if (error) return <ErrorMessage error={error} />;
  
  return (
    <div>
      <PricingFilters filter={filter} onFilterChange={setFilter} />
      <PricingTable
        data={pricingData}
        onPriceUpdate={handlePriceUpdate}
      />
    </div>
  );
}

GraphQL Integration

GraphQL Client Setup

// lib/graphql/client.ts
import { GraphQLClient } from 'graphql-request';
import { getSession } from 'next-auth/react';

const graphqlEndpoint = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT!;

export const graphqlClient = new GraphQLClient(graphqlEndpoint, {
  credentials: 'include',
  requestMiddleware: async (request) => {
    const session = await getSession();
    
    if (session?.accessToken) {
      request.headers = {
        ...request.headers,
        Authorization: `Bearer ${session.accessToken}`,
      };
    }
    
    return request;
  },
});

GraphQL Queries

// lib/graphql/queries.ts
import { graphqlClient } from './client';
import { gql } from 'graphql-request';

export const GET_PRICES_QUERY = gql`
  query GetPrices($storeId: ID!, $filter: PriceFilter) {
    prices(storeId: $storeId, filter: $filter) {
      id
      storeId
      productId
      productName
      amount
      currency
      effectiveDate
      expirationDate
      category
    }
  }
`;

export const UPDATE_PRICE_MUTATION = gql`
  mutation UpdatePrice($input: UpdatePriceInput!) {
    updatePrice(input: $input) {
      id
      amount
      effectiveDate
      expirationDate
    }
  }
`;

export async function getPricingData(variables: {
  storeId: string;
  filter?: PriceFilter;
}) {
  return await graphqlClient.request(GET_PRICES_QUERY, variables);
}

Real-Time Data Synchronization

// hooks/use-realtime-pricing.ts
import { useEffect } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { graphqlClient } from '@/lib/graphql/client';

export function useRealtimePricing(storeId: string) {
  const queryClient = useQueryClient();
  
  useEffect(() => {
    if (!storeId) return;
    
    // GraphQL subscription for real-time updates
    const subscription = graphqlClient.subscribe(
      `
        subscription OnPriceUpdate($storeId: ID!) {
          onPriceUpdate(storeId: $storeId) {
            id
            amount
            effectiveDate
          }
        }
      `,
      { storeId },
      {
        next: (data) => {
          // Update React Query cache with new data
          queryClient.setQueryData(['pricing', storeId], (oldData: any) => {
            if (!oldData) return oldData;
            
            return oldData.map((price: any) =>
              price.id === data.onPriceUpdate.id
                ? { ...price, ...data.onPriceUpdate }
                : price
            );
          });
        },
        error: (error) => {
          console.error('Subscription error:', error);
        },
      }
    );
    
    return () => {
      subscription.unsubscribe();
    };
  }, [storeId, queryClient]);
}

Material-UI Component Library

Material-UI Setup

// app/layout.tsx
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import { theme } from '@/lib/theme';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ThemeProvider theme={theme}>
          <CssBaseline />
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Material-UI Components

// components/pricing/PricingTable.tsx
'use client';

import {
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Paper,
  TextField,
  Button,
  Chip,
} from '@mui/material';
import { Edit, Save, Cancel } from '@mui/icons-material';

interface PricingTableProps {
  data: Price[];
  onPriceUpdate: (priceId: string, amount: number) => Promise<void>;
}

export function PricingTable({ data, onPriceUpdate }: PricingTableProps) {
  const [editingId, setEditingId] = useState<string | null>(null);
  const [editValue, setEditValue] = useState<string>('');
  
  return (
    <TableContainer component={Paper}>
      <Table>
        <TableHead>
          <TableRow>
            <TableCell>Product</TableCell>
            <TableCell>Current Price</TableCell>
            <TableCell>Effective Date</TableCell>
            <TableCell>Status</TableCell>
            <TableCell>Actions</TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {data.map((price) => (
            <TableRow key={price.id}>
              <TableCell>{price.productName}</TableCell>
              <TableCell>
                {editingId === price.id ? (
                  <TextField
                    type="number"
                    value={editValue}
                    onChange={(e) => setEditValue(e.target.value)}
                    size="small"
                  />
                ) : (
                  `$${price.amount.toFixed(2)}`
                )}
              </TableCell>
              <TableCell>{formatDate(price.effectiveDate)}</TableCell>
              <TableCell>
                <Chip
                  label={price.isActive ? 'Active' : 'Inactive'}
                  color={price.isActive ? 'success' : 'default'}
                  size="small"
                />
              </TableCell>
              <TableCell>
                {editingId === price.id ? (
                  <>
                    <Button
                      size="small"
                      startIcon={<Save />}
                      onClick={async () => {
                        await onPriceUpdate(price.id, parseFloat(editValue));
                        setEditingId(null);
                      }}
                    >
                      Save
                    </Button>
                    <Button
                      size="small"
                      startIcon={<Cancel />}
                      onClick={() => setEditingId(null)}
                    >
                      Cancel
                    </Button>
                  </>
                ) : (
                  <Button
                    size="small"
                    startIcon={<Edit />}
                    onClick={() => {
                      setEditingId(price.id);
                      setEditValue(price.amount.toString());
                    }}
                  >
                    Edit
                  </Button>
                )}
              </TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  );
}

Authentication and Authorization

NextAuth.js Configuration

// lib/auth.ts
import NextAuth, { NextAuthOptions } from 'next-auth';
import CognitoProvider from 'next-auth/providers/cognito';

export const authOptions: NextAuthOptions = {
  providers: [
    CognitoProvider({
      clientId: process.env.COGNITO_CLIENT_ID!,
      clientSecret: process.env.COGNITO_CLIENT_SECRET!,
      issuer: process.env.COGNITO_ISSUER!,
    }),
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      if (account) {
        token.accessToken = account.access_token;
        token.idToken = account.id_token;
      }
      return token;
    },
    async session({ session, token }) {
      session.accessToken = token.accessToken;
      session.user.storeIds = token.storeIds;
      session.user.role = token.role;
      return session;
    },
  },
  pages: {
    signIn: '/login',
  },
};

export default NextAuth(authOptions);

Role-Based Access Control

// middleware.ts
import { withAuth } from 'next-auth/middleware';
import { NextResponse } from 'next/server';

export default withAuth(
  function middleware(req) {
    const token = req.nextauth.token;
    const path = req.nextUrl.pathname;
    
    // Role-based route protection
    if (path.startsWith('/pricing/admin') && token?.role !== 'admin') {
      return NextResponse.redirect(new URL('/pricing', req.url));
    }
    
    if (path.startsWith('/pricing') && !token?.storeIds?.length) {
      return NextResponse.redirect(new URL('/unauthorized', req.url));
    }
    
    return NextResponse.next();
  },
  {
    callbacks: {
      authorized: ({ token }) => !!token,
    },
  }
);

export const config = {
  matcher: ['/pricing/:path*'],
};

Protected Route Example

// app/pricing/admin/page.tsx
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';

export default async function AdminPricingPage() {
  const session = await getServerSession(authOptions);
  
  if (!session) {
    redirect('/login');
  }
  
  if (session.user.role !== 'admin') {
    redirect('/pricing');
  }
  
  return (
    <div>
      <h1>Admin Pricing Management</h1>
      {/* Admin-only content */}
    </div>
  );
}

SST Serverless Deployment

SST Configuration

// sst.config.ts
import { NextjsSite } from 'sst/constructs';

export default {
  config(_input) {
    return {
      name: 'pricing-frontend',
      region: 'us-east-1',
    };
  },
  stacks(app) {
    app.stack(function Site({ stack }) {
      const site = new NextjsSite(stack, 'PricingFrontend', {
        path: '.',
        environment: {
          NEXT_PUBLIC_GRAPHQL_ENDPOINT: process.env.GRAPHQL_ENDPOINT!,
          NEXT_PUBLIC_COGNITO_CLIENT_ID: process.env.COGNITO_CLIENT_ID!,
          NEXT_PUBLIC_COGNITO_ISSUER: process.env.COGNITO_ISSUER!,
        },
        bind: [
          // Bind AWS resources for access
          pricingServiceApi,
          cognitoUserPool,
        ],
      });
      
      stack.addOutputs({
        SiteUrl: site.url,
      });
    });
  },
};

SST Deployment Architecture

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   CloudFront CDN                        │
│   - Global edge caching                 │
│   - Fast content delivery               │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   AWS Lambda@Edge                       │
│   - Next.js SSR execution                │
│   - API routes                           │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   S3 Bucket                             │
│   - Static assets (JS, CSS, images)      │
│   - Pre-rendered pages                  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Deployment Process

# 1. Install dependencies
pnpm install

# 2. Build Next.js application
pnpm build

# 3. Deploy with SST
sst deploy

# SST automatically:
# - Builds Next.js app
# - Creates Lambda functions for SSR
# - Uploads static assets to S3
# - Configures CloudFront CDN
# - Sets up environment variables

Code Structure and Patterns

Project Structure

pricing-frontend/
ā”œā”€ā”€ app/                    # Next.js App Router
│   ā”œā”€ā”€ layout.tsx         # Root layout
│   ā”œā”€ā”€ page.tsx           # Home page
│   ā”œā”€ā”€ pricing/           # Pricing section
│   │   ā”œā”€ā”€ layout.tsx
│   │   ā”œā”€ā”€ page.tsx
│   │   └── [storeId]/
│   │       └── page.tsx
│   └── api/               # API routes
│       └── auth/
│           └── [...nextauth]/
│               └── route.ts
ā”œā”€ā”€ components/            # React components
│   ā”œā”€ā”€ pricing/
│   │   ā”œā”€ā”€ PricingTable.tsx
│   │   ā”œā”€ā”€ PricingFilters.tsx
│   │   └── PricingDashboard.tsx
│   └── common/
│       ā”œā”€ā”€ Button.tsx
│       └── LoadingSpinner.tsx
ā”œā”€ā”€ hooks/                # Custom React hooks
│   ā”œā”€ā”€ use-pricing-data.ts
│   ā”œā”€ā”€ use-realtime-pricing.ts
│   └── use-update-price.ts
ā”œā”€ā”€ atoms/                # Jotai atoms
│   └── pricing-atoms.ts
ā”œā”€ā”€ lib/                  # Utilities
│   ā”œā”€ā”€ graphql/
│   │   ā”œā”€ā”€ client.ts
│   │   └── queries.ts
│   ā”œā”€ā”€ auth.ts
│   └── theme.ts
ā”œā”€ā”€ types/                # TypeScript types
│   └── pricing.ts
ā”œā”€ā”€ sst.config.ts         # SST configuration
ā”œā”€ā”€ next.config.js        # Next.js configuration
ā”œā”€ā”€ tsconfig.json         # TypeScript configuration
└── package.json

Next.js Patterns Used

1. Server and Client Components

// Server Component (default)
// app/pricing/page.tsx
export default async function PricingPage() {
  const data = await fetchPricingData(); // Server-side
  return <PricingDashboard initialData={data} />;
}

// Client Component (with 'use client')
// components/pricing/PricingDashboard.tsx
'use client';

export function PricingDashboard({ initialData }) {
  const [state, setState] = useState(); // Client-side
  return <div>...</div>;
}

2. Parallel Data Fetching

// app/pricing/page.tsx
export default async function PricingPage() {
  // Fetch multiple data sources in parallel
  const [pricingData, storeData, userData] = await Promise.all([
    fetchPricingData(),
    fetchStoreData(),
    fetchUserData(),
  ]);
  
  return (
    <PricingDashboard
      pricingData={pricingData}
      storeData={storeData}
      userData={userData}
    />
  );
}

3. Streaming with Suspense

// app/pricing/page.tsx
import { Suspense } from 'react';

export default function PricingPage() {
  return (
    <div>
      <Suspense fallback={<FiltersSkeleton />}>
        <PricingFilters />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <PricingTable />
      </Suspense>
    </div>
  );
}

4. Error Boundaries

// app/pricing/error.tsx
'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error;
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

5. Loading States

// app/pricing/loading.tsx
export default function Loading() {
  return (
    <div className="flex items-center justify-center h-screen">
      <LoadingSpinner />
      <p>Loading pricing data...</p>
    </div>
  );
}

Performance Optimization

Next.js Optimizations

1. Image Optimization

// components/common/OptimizedImage.tsx
import Image from 'next/image';

export function OptimizedImage({ src, alt }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      priority // Preload important images
      placeholder="blur" // Blur placeholder
    />
  );
}

2. Code Splitting

// Dynamic imports for code splitting
import dynamic from 'next/dynamic';

const PricingReports = dynamic(
  () => import('@/components/pricing/PricingReports'),
  {
    loading: () => <LoadingSpinner />,
    ssr: false, // Client-only component
  }
);

3. Font Optimization

// app/layout.tsx
import { Inter } from 'next/font/google';

const inter = Inter({ subsets: ['latin'] });

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  );
}

4. Bundle Analysis

# Analyze bundle size
ANALYZE=true pnpm build

# Output shows:
# - Bundle sizes
# - Code splitting effectiveness
# - Duplicate dependencies

React Query Optimizations

// Optimized React Query configuration
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 30000, // 30 seconds
      cacheTime: 300000, // 5 minutes
      refetchOnWindowFocus: true,
      refetchOnReconnect: true,
      retry: 1,
    },
  },
});

Fast Loading Times

Achieved through:

  1. Server-Side Rendering (initial HTML with data)
  2. Code splitting (load only needed code)
  3. Image optimization (Next.js Image component)
  4. Font optimization (next/font)
  5. React Query caching (reduce API calls)
  6. CloudFront CDN (edge caching)
  7. Lambda@Edge (SSR at edge locations)

Development Workflow

Local Development

# 1. Install Node.js (v18+)
node --version  # v18.x.x

# 2. Install dependencies
pnpm install

# 3. Set up environment variables
cp .env.example .env.local
# Edit .env.local with your values

# 4. Run development server
pnpm dev
# Starts Next.js dev server on http://localhost:3000

# 5. Run type checking
pnpm type-check

# 6. Run linting
pnpm lint

# 7. Run tests
pnpm test

Testing Next.js Code

// __tests__/components/PricingTable.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { PricingTable } from '@/components/pricing/PricingTable';

describe('PricingTable', () => {
  const mockData = [
    {
      id: '1',
      productName: 'Taco',
      amount: 1.99,
      effectiveDate: '2024-01-01',
    },
  ];
  
  it('renders pricing data', () => {
    render(<PricingTable data={mockData} onPriceUpdate={jest.fn()} />);
    expect(screen.getByText('Taco')).toBeInTheDocument();
    expect(screen.getByText('$1.99')).toBeInTheDocument();
  });
  
  it('allows editing prices', () => {
    const onUpdate = jest.fn();
    render(<PricingTable data={mockData} onPriceUpdate={onUpdate} />);
    
    fireEvent.click(screen.getByText('Edit'));
    fireEvent.change(screen.getByDisplayValue('1.99'), {
      target: { value: '2.49' },
    });
    fireEvent.click(screen.getByText('Save'));
    
    expect(onUpdate).toHaveBeenCalledWith('1', 2.49);
  });
});

Debugging Next.js

// Enable Next.js debugging
// next.config.js
module.exports = {
  // Enable source maps in production
  productionBrowserSourceMaps: true,
  
  // Logging
  logging: {
    fetches: {
      fullUrl: true,
    },
  },
};

// Client-side debugging
'use client';

export function PricingDashboard() {
  useEffect(() => {
    // React DevTools
    console.log('Component mounted');
    
    // Next.js DevTools
    if (process.env.NODE_ENV === 'development') {
      console.log('Development mode');
    }
  }, []);
}

CI/CD Pipeline

# .github/workflows/deploy.yml
name: Deploy Pricing Frontend

on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18.x'
      
      - name: Setup pnpm
        uses: pnpm/action-setup@v2
        with:
          version: 8
      
      - name: Install dependencies
        run: pnpm install --frozen-lockfile
      
      - name: Type check
        run: pnpm type-check
      
      - name: Lint
        run: pnpm lint
      
      - name: Test
        run: pnpm test
      
      - name: Build
        run: pnpm build
        env:
          NEXT_PUBLIC_GRAPHQL_ENDPOINT: ${{ secrets.GRAPHQL_ENDPOINT }}
          NEXT_PUBLIC_COGNITO_CLIENT_ID: ${{ secrets.COGNITO_CLIENT_ID }}
      
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      
      - name: Deploy with SST
        run: |
          pnpm add -g sst
          sst deploy --stage production

Summary

The pricing-frontend is built entirely on Next.js 14, React 18, and TypeScript through:

  1. Next.js 14 App Router for modern routing and SSR
  2. React 18 with concurrent features for smooth UI updates
  3. TypeScript for type safety throughout
  4. Server-Side Rendering for fast initial page loads
  5. Jotai for lightweight UI state management
  6. React Query for server state, caching, and synchronization
  7. GraphQL integration with real-time subscriptions
  8. Material-UI for consistent, professional UI components
  9. NextAuth.js with Cognito for authentication and RBAC
  10. SST Framework for serverless deployment on AWS

The application demonstrates production-grade Next.js expertise at enterprise scale:

  • Real-time franchisee pricing for 8,000+ locations
  • Fast loading times through SSR and optimization techniques
  • Streamlined pricing operations for Taco Bell staff
  • Role-based access control for secure pricing management
  • Serverless architecture with automatic scaling

This architecture showcases deep understanding of Next.js runtime characteristics, React 18 concurrent features, serverless deployment patterns, and high-performance TypeScript/JavaScript development for enterprise-scale pricing management systems.