Next.js Frontend
Master uses Next.js as its front end. Not a thin client, not a few React widgets bolted onto server-rendered HTML — the complete App Router framework, living in frontend/. It is where your whole stack becomes something a person can see, click, and use.

Why Next.js is the front end#
Plenty of Node frameworks render HTML strings on the server. Master takes a different stance: the UI deserves its own first-class framework. By making Next.js the front end, every Master app gets, for free:
- React Server Components — fetch data on the server and stream finished HTML; no client-side loading spinners for the first view.
- File-based App Router — folders become routes, with nested layouts, loading and error states.
- Built-in optimization — automatic code-splitting, image optimization, font handling, and prefetching.
- A real component model — compose your UI from reusable React components, not template partials.
- SEO & performance — server rendering and streaming give you fast, crawlable pages out of the box.
api() helper.The api() helper — one doorway to the backend#
Every call from the front end to the MasterController API flows through a single typed helper at frontend/app/lib/api.ts. It centralizes the base URL, headers, and error handling.
const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001';
export async function api<T = unknown>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${BASE_URL}${path}`, {
cache: 'no-store',
headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
...init,
});
if (!res.ok) throw new Error(`API ${res.status}`);
return res.json() as Promise<T>;
}Fetching data: Server Components#
Most pages are async Server Components. They run on the server, call the API directly (server-to-server, so no CORS and no client round-trip), and render HTML the browser receives ready to display.
import Link from 'next/link';
import { api } from '../lib/api';
interface Post { id: number; title: string }
export default async function PostsPage() {
const { data } = await api<{ data: Post[] }>('/posts');
return (
<main>
<h1>Posts</h1>
<ul>
{data.map((p) => (
<li key={p.id}><Link href={`/posts/${p.id}`}>{p.title}</Link></li>
))}
</ul>
</main>
);
}Mutating data: Client Components#
Interactivity — forms, buttons, optimistic updates — lives in 'use client' components that call the same api() helper from the browser.
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '../lib/api';
export function NewPostForm() {
const [title, setTitle] = useState('');
const router = useRouter();
async function onSubmit(e: React.FormEvent) {
e.preventDefault();
await api('/posts', { method: 'POST', body: JSON.stringify({ title }) });
setTitle('');
router.refresh(); // re-run the server components to show the new post
}
return (
<form onSubmit={onSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Create</button>
</form>
);
}router.refresh() re-renders the surrounding Server Components, so the new data appears without a full page reload — a smooth, app-like experience backed by real server data.
Routing & layouts#
The App Router maps folders to URLs. Add frontend/app/about/page.tsx and you have /about — or generate it with master generate page about. Shared chrome lives in layout.tsx; per-route loading and error UI live in loading.tsx and error.tsx.
Server vs browser calls & CORS#
Server Components call the API server-to-server, so CORS never applies. Client Components call it from the browser, where the backend’s CORS config allows your frontend origin. Prefer same-origin? Add a Next.js rewrite to proxy /api/* to the backend.
const nextConfig = {
async rewrites() {
return [{ source: '/api/:path*', destination: 'http://localhost:3001/:path*' }];
},
};
export default nextConfig;Configuration#
The frontend reaches the backend through NEXT_PUBLIC_API_URL. master dev sets it automatically in development; in production, point it at your public API URL. That one variable is the only wiring the front end needs.
NEXT_PUBLIC_API_URLand add the frontend origin to the API’s CORS list. See Deployment.