React / Next.js project structure (App Router)
The ZEUS frontend uses Next.js 15 with the App Router. A consistent directory structure and a clear boundary between server and client components are the foundation on which we build the rest.
Directory structure
app/
├── (dashboard)/
│ ├── layout.tsx
│ └── probes/
│ ├── page.tsx # Server Component (by default)
│ └── probe-table.tsx # "use client" — interactive
├── api/
│ └── probes/route.ts # Route Handler
├── layout.tsx
└── globals.css
components/ # shared, reusable UI
lib/ # logic, API clients, utils
hooks/ # custom hooks (use client)
types/ # shared types
ProfessNet standard: the
app/directory is solely for routing and page composition. Reusable UI lives incomponents/, logic inlib/.page.tsxcontains no heavy logic — it calls functions fromlib/.
Server vs Client Components
In the App Router components are server-side by default. They render on the server, don't reach the bundle and can read data directly.
// app/(dashboard)/probes/page.tsx — Server Component
import { getProbes } from "@/lib/probes";
import { ProbeTable } from "./probe-table";
export default async function ProbesPage() {
const probes = await getProbes(); // server-side fetch
return <ProbeTable probes={probes} />;
}
We add "use client" only where interactivity is needed — state, events,
browser hooks.
// app/(dashboard)/probes/probe-table.tsx
"use client";
import { useState } from "react";
import type { Probe } from "@/types/probe";
export function ProbeTable({ probes }: { probes: Probe[] }) {
const [filter, setFilter] = useState("");
const shown = probes.filter((p) => p.host.includes(filter));
return (
<>
<input value={filter} onChange={(e) => setFilter(e.target.value)} />
{shown.map((p) => (
<Row key={p.id} probe={p} />
))}
</>
);
}
ProfessNet standard:
"use client"goes as low as possible in the tree. Don't mark the whole page as client-side just for one button — extract a small client component and leave the rest on the server.
Naming conventions
| Element | Convention | Example |
|---|---|---|
| Component (file) | kebab-case | probe-table.tsx |
| Component (export) | PascalCase | ProbeTable |
| Hook | use + camelCase | useStudioData |
| Type / interface | PascalCase | Probe, ScanResult |
| Next special files | reserved | page, layout, loading, error |
Imports with an alias
We configure the @/ alias instead of relative ../../../.
{ "compilerOptions": { "paths": { "@/*": ["./*"] } } }
import { ProbeTable } from "@/components/probe-table"; // readable
Tip:
loading.tsxanderror.tsxfiles in a segment automatically give you loading and error states for the whole subpage — use them instead of manual spinners everywhere.
Server by default, client only where needed, logic in lib/ — that's a
structure that scales together with the ZEUS console and keeps the bundle on
the browser side small.