Wordloop Platform
Platform ServicesApp (Next.js)

App Implementation Guide

Concrete Next.js patterns for Server Actions, Client Providers, and UI separation.

App Implementation Guide (Next.js)

This guide translates WordLoop's overarching Engineering Principles into explicit, copy-pasteable React/Next.js code for the wordloop-app service.

1. Concrete Trace-First Development

Next.js automatically instruments App Router requests with OpenTelemetry. However, when we perform background mutations or explicit fetch requests to the backend proxy, we dynamically enrich the trace.

Identity Propagation (Baggage)

The frontend is responsible for injecting the authenticated user's ID into the W3C Baggage header. This guarantees that all downstream services (Core, ML) can attribute their database queries directly back to the user without fetching identity twice.

// lib/providers/fetcher.ts
import { auth } from '@clerk/nextjs/server';
 
export const customFetch = async (url: string, options: RequestInit) => {
  const { userId, getToken } = await auth();
  const token = await getToken();
 
  const headers = new Headers(options.headers);
  if (token) headers.set('Authorization', `Bearer ${token}`);
  if (userId) headers.set('Baggage', `enduser.id=${userId}`);
 
  return fetch(url, { ...options, headers });
};

2. Concrete Error Handling (Server Actions)

We never throw naked exceptions from Server Actions to Client Components, as this causes hard React Error Boundary crashes. We utilize the Result Pattern to treat errors as standard data.

The Result Pattern

Server Actions return an explicitly typed object containing either the data or the error message, forcing the frontend component to handle failure states gracefully.

// app/actions/meetings.ts
'use server'
 
import { getMeetingClient } from '@/lib/providers/api';
 
export type ActionState<T> = 
  | { success: true; data: T }
  | { success: false; error: string };
 
export async function createMeetingAction(formData: FormData): Promise<ActionState<string>> {
  try {
    const title = formData.get('title') as string;
    const client = await getMeetingClient();
    
    // Attempt the mutation via generated P&A Provider
    const result = await client.createMeeting({ title });
    
    return { success: true, data: result.meetingId };
  } catch (error) {
    // Map network/backend errors to a graceful UI message
    return { success: false, error: "Failed to create meeting. Please try again." };
  }
}

Graceful Component Degradation

The React component consumes this pattern directly without needing try/catch blocks.

// components/submit-button.tsx
'use client'
 
import { createMeetingAction } from '@/app/actions/meetings';
import { useState } from 'react';
 
export function SubmitMeeting() {
  const [error, setError] = useState<string | null>(null);
 
  const handleSubmit = async (formData: FormData) => {
    const result = await createMeetingAction(formData);
    if (!result.success) {
      setError(result.error);
      return;
    }
    // Handle success (e.g., router.push)
  };
 
  return (
    <form action={handleSubmit}>
      {error && <div className="text-red-500">{error}</div>}
      <button type="submit">Create</button>
    </form>
  );
}

3. Concrete Dependency Injection (Providers)

Rather than handwriting brittle fetch calls scattered across multiple UI components, we rely entirely on purely generated API clients.

Using the Generated Orval Client

Orval reads our OpenAPI spec and generates pure TypeScript hooks and fetchers. These act as our "Providers" in the Clean Architecture context. Components (the Domain) use them without caring about the underlying HTTP mechanism.

// components/meeting-list.tsx
'use client'
 
// 1. Import the generated Provider
import { useGetMeetings } from '@/lib/providers/generated/wordloop';
 
export function MeetingList() {
  // 2. The Provider abstracts SWR caching, headers, and type validation
  const { data, error, isLoading } = useGetMeetings();
 
  if (isLoading) return <Skeleton />
  if (error) return <ErrorMessage error={error} />
  
  // 3. Types are guaranteed perfectly backwards compatible with Core
  return (
    <ul>
      {data.meetings.map(m => <li key={m.id}>{m.title}</li>)}
    </ul>
  )
}

4. Idiomatic React & TypeScript Standards

We do not aim to rewrite foundational guidance on writing excellent React and TypeScript code. Instead, we adhere to established industry baselines mapped to our internal engineering principles.

We expect all Wordloop App engineers to intimately understand:

Below is concrete guidance on how overarching TS/React idioms manifest as system-enforced architectural invariants.

Default to Server Components (Clean Architecture)

The React Idiom: Start with React Server Components (RSC) and only use 'use client' at the absolute leaf nodes.
The Principle Connection: As defined in our Service Architecture, RSCs act as our "Inbound Adapters." They handle pure data fetching securely on the backend without exposing network waterfalls to the client. This enforces a strict separation where UI interactivity (Client components) is totally decoupled from data orchestration.

// app/meetings/page.tsx
// This is a Server Component by default. No 'use client' directive.
 
import { getMeetingClient } from '@/lib/providers/api';
import { MeetingList } from '@/components/MeetingList';
 
export default async function Page() {
  // 1. Data orchestration stays securely on the server.
  const client = await getMeetingClient();
  const meetings = await client.listMeetings();
  
  // 2. We pass pure data down to the interactivity leaf.
  return (
    <main>
      <h1>Your Meetings</h1>
      <MeetingList initialData={meetings} />
    </main>
  );
}

Discriminated Unions for Predictable State (Resilience)

The TypeScript Idiom: Using strict discriminated union types instead of optional properties.
The Principle Connection: We avoid try/catch UI crashes by mapping server actions to unified result patterns. Using discriminated unions guarantees the TypeScript compiler will force the frontend engineer to handle both states explicitly, leading to Resilient Error Handling.

// 1. The Discriminated Union explicitly separates the Success and Failure states.
export type ActionState<T> = 
  | { success: true; data: T }
  | { success: false; error: string };
 
// 2. The UI is forced to check the discriminator before accessing data.
function handleResponse(response: ActionState<Meeting>) {
  if (!response.success) {
    // TS knows 'response' only has an 'error' here.
    showToast(response.error);
    return;
  }
  
  // TS knows 'response' guaranteed has 'data' here.
  renderMeeting(response.data);
}