Skip to content

Frontend Development Guide

This guide covers the day-to-day development workflow for the 2Sigma frontend (ai-tutor-ui), a Next.js 16 application.

Prerequisites

Before starting, ensure you've completed the Getting Started Guide.

Daily Workflow

# 1. Pull latest changes
git pull origin main

# 2. Install dependencies (if package.json changed)
npm install

# 3. Start dev server
npm run dev

The app runs at http://localhost:3000.

Hot Reload

Next.js Fast Refresh is automatic. The browser updates instantly when you save a file, preserving component state. No manual restart needed.

Restart required for env changes

Changes to .env.local require stopping and restarting the dev server. Fast Refresh doesn't pick up environment variable changes.

Environment Variables

All client-side variables must start with NEXT_PUBLIC_. Without that prefix, they're invisible to the browser.

Config file: .env.local (never committed, listed in .gitignore). Copy .env.local.example to get started:

cp .env.local.example .env.local
Variable Required Description
NEXT_PUBLIC_API_BASE_URL Yes Backend API URL (e.g., http://localhost:9898/api/v1)
NEXT_PUBLIC_APP_NAME No Application display name (default: "2Sigma")

Code Organization & Naming Conventions

Project Structure

ai-tutor-ui/
├── app/                     # Next.js App Router pages
│   ├── layout.tsx           # Root layout
│   ├── page.tsx             # Home route
│   ├── globals.css          # Global styles + CSS custom properties
│   ├── dashboard/
│   ├── courses/
│   ├── signup-onboarding/
│   └── chat-debug/
├── components/              # Shared React components
│   ├── ui/                  # shadcn/ui primitives
│   └── [feature]/           # Feature-specific components
├── hooks/                   # Custom React hooks
├── lib/
│   ├── api/                 # API client functions
│   ├── validations/         # Zod schemas for forms
│   ├── authClient.ts        # apiFetch + token management
│   └── utils.ts             # Shared utilities
├── types/                   # TypeScript type definitions
├── tests/                   # Test setup and utilities
│   ├── setup.ts
│   ├── test-utils.tsx
│   └── mocks/               # MSW request handlers
└── public/                  # Static assets

Naming Conventions

Pages/Routes — kebab-case directories under app/:

app/signup-onboarding/
app/chat-debug/
app/courses/

Components — PascalCase .tsx files:

CourseDialog.tsx
ModuleTree.tsx
Header.tsx

Hooksuse- prefix, kebab-case files:

hooks/use-debounce.ts
hooks/use-toast.ts

API files — kebab-case matching the resource:

lib/api/courses.ts
lib/api/content-types.ts
lib/api/llm-providers.ts

Types — kebab-case files in types/:

types/course.ts
types/interactive-ui.ts
types/llm-provider.ts

Validations — kebab-case in lib/validations/:

lib/validations/auth.ts
lib/validations/course.ts
lib/validations/module.ts
lib/validations/content.ts
lib/validations/onboarding.ts

Test co-location__tests__/ directories adjacent to source files:

app/__tests__/
lib/__tests__/
lib/api/__tests__/

Adding a New Page

  1. Create a directory under app/ (e.g., app/my-feature/)
  2. Add page.tsx as the route component
  3. Optionally add layout.tsx, loading.tsx, or error.tsx alongside it
  4. Create feature components in components/my-feature/ if the page needs them
app/my-feature/
├── page.tsx        # Required — the route
├── layout.tsx      # Optional — wraps this route's subtree
├── loading.tsx     # Optional — shown while page suspends
└── error.tsx       # Optional — error boundary for this route

App Router conventions

Next.js uses the App Router. Every file named page.tsx becomes a route. Files named layout.tsx, loading.tsx, and error.tsx are special and handled automatically by the framework.

Adding a New API Resource

Follow these steps to wire up a new backend resource end-to-end.

1. Create type definitions in types/my-resource.ts

2. Create API functions in lib/api/my-resource.ts using apiFetch:

// lib/api/my-resource.ts
import { apiFetch } from "../authClient"

export interface MyResource {
  id: number
  name: string
}

export async function listMyResources(): Promise<MyResource[]> {
  return apiFetch<MyResource[]>("/my-resources/")
}

export async function createMyResource(data: { name: string }): Promise<MyResource> {
  return apiFetch<MyResource>("/my-resources/", {
    method: "POST",
    body: data,
  })
}

apiFetch handles auth headers, JSON serialization, and error parsing automatically. It reads the token from localStorage and throws an ApiError on non-2xx responses.

3. Create Zod validation schemas in lib/validations/my-resource.ts if the resource involves forms:

// lib/validations/my-resource.ts
import { z } from "zod"

export const myResourceSchema = z.object({
  name: z.string().min(1, "Name is required"),
})

export type MyResourceFormData = z.infer<typeof myResourceSchema>

Adding a shadcn/ui Component

shadcn/ui components live in components/ui/. They're not installed as a package dependency — the CLI copies the source directly into your project so you can customize freely.

npx shadcn@latest add [component-name]

For example:

npx shadcn@latest add calendar
npx shadcn@latest add data-table

Each component uses Radix UI primitives for accessibility, Tailwind for styling, and class-variance-authority (cva) for variant management. Global theme tokens (colors, radius, etc.) are defined as CSS custom properties in app/globals.css.

Testing

Framework

  • Vitest — test runner (fast, Vite-native)
  • React Testing Library — component rendering and queries
  • MSW (Mock Service Worker) — intercepts fetch calls at the network level

Commands

Command Description
npm run test Watch mode — re-runs on file changes
npm run test:run Single run — for CI
npm run test:coverage With coverage report (v8 provider)
npm run test:ui Visual Vitest UI in the browser

Test Setup

tests/setup.ts runs before every test file. It:

  • Mocks next/navigation (router, pathname, search params)
  • Mocks localStorage with an in-memory store
  • Starts the MSW server before all tests and resets handlers after each test

tests/test-utils.tsx re-exports React Testing Library's render wrapped with app providers. Always import from here instead of @testing-library/react directly:

import { render, screen } from '@/tests/test-utils'

tests/mocks/ contains MSW request handlers that intercept API calls during tests. Add handlers here to mock specific endpoints.

Example Test

import { render, screen } from '@/tests/test-utils'
import { MyComponent } from '../MyComponent'

describe('MyComponent', () => {
  it('renders correctly', () => {
    render(<MyComponent />)
    expect(screen.getByText('Expected text')).toBeInTheDocument()
  })
})

Test file location

Place test files in a __tests__/ directory adjacent to the source file being tested. Name them *.test.ts or *.test.tsx.

Linting

ESLint is configured with next/core-web-vitals, which includes React, accessibility, and Next.js-specific rules.

npm run lint

Run this before committing. The CI pipeline will catch lint errors, but it's faster to fix them locally.

Docker

The Dockerfile uses a two-stage build:

  1. Builder (node:20-alpine) — installs dependencies and runs next build
  2. Runner (node:20-alpine) — copies only the standalone output, no dev dependencies

output: 'standalone' in next.config.js produces a minimal self-contained build. The image runs on port 3000.

Build args are baked in at image build time (not runtime):

docker build \
  --build-arg NEXT_PUBLIC_APP_NAME="2Sigma" \
  --build-arg NEXT_PUBLIC_API_BASE_URL="https://api.example.com/api/v1" \
  -t ai-tutor-ui .

Build-time env vars

NEXT_PUBLIC_* variables are inlined into the JavaScript bundle during next build. Changing them requires rebuilding the image — you can't override them at container runtime.

Troubleshooting

CORS errors

The backend must include http://localhost:3000 in its BACKEND_CORS_ORIGINS setting. Check your backend .env:

BACKEND_CORS_ORIGINS=["http://localhost:3000", "http://localhost:8000"]

Hydration mismatch

This usually means you're reading localStorage, window, or another browser-only API during server-side rendering. Wrap the access in a useEffect:

useEffect(() => {
  const value = localStorage.getItem('my-key')
  // safe to use here
}, [])

Module not found

Run npm install first. If the error persists, check the import path. The @/ alias maps to the project root, so @/components/ui/button resolves to ./components/ui/button.

Git Workflow

Branch Naming

  • Features: feature/short-description
  • Bug fixes: fix/short-description
  • Docs: docs/short-description

Commit Messages

Follow conventional commits:

feat: add course enrollment page
fix: resolve token refresh loop
docs: update frontend dev guide
refactor: simplify apiFetch error handling
test: add unit tests for CourseDialog

Pre-Commit Checklist

  • Lint: npm run lint
  • Tests: npm run test:run
  • Build: npm run build
  • Update docs if needed

Next Steps