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 in components/, logic in lib/. page.tsx contains no heavy logic — it calls functions from lib/.

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

ElementConventionExample
Component (file)kebab-caseprobe-table.tsx
Component (export)PascalCaseProbeTable
Hookuse + camelCaseuseStudioData
Type / interfacePascalCaseProbe, ScanResult
Next special filesreservedpage, 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.tsx and error.tsx files 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.