Master Guides

Next.js Frontend

The face of your app — a real Next.js application, fully integrated.

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.

The gateway to the realm — the frontend

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.
The front end stays a front end
Because the UI is a separate Next.js app, your React code never touches SQL and your API code never renders HTML. The two halves meet at one clean, typed boundary — the 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.

frontend/app/lib/api.ts
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.

frontend/app/posts/page.tsx
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.

frontend/app/posts/new-post-form.tsx
'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.

frontend/next.config.mjs
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.

Deploy the front end anywhere
Because it’s a standard Next.js app, you can host the frontend on a platform like Vercel and run the MasterController API on any Node host — just set NEXT_PUBLIC_API_URLand add the frontend origin to the API’s CORS list. See Deployment.