Przejdź do głównej zawartości

Conventions

This document outlines the coding conventions, architectural patterns, and best practices established in the ToPWR Admin Panel project. For situations not covered in this document, the standard Solvro web development documentation applies.

Feature-Based Architecture

Import Restrictions

Do not import deeply from features. Only import from the feature root or its top-level files.

// ✅ Good - Import from feature root
import { Resource, getResourceMetadata } from "@/features/resources";
import { AbstractResourceForm } from "@/features/abstract-resource-form";
// ✅ Good - Import from feature entry point files
import { getAuthStateServer } from "@/features/authentication/server";
import { getResourceMetadata } from "@/features/resources/node";
// ❌ Bad - Deep imports into feature internals
import type { ResourceMetadata } from "@/features/resources/types/metadata";
import { FormInputs } from "@/features/abstract-resource-form/components/inputs";

Features are free to define internal types, utilities and structures, and will only re-export the code they consider “public-facing”, that is suitable to be imported from outside of the feature, in one of the feature top-level files.

On the other hand, the code within a specific feature should always use relative imports when importing code inside of that same feature. This is to visually separate the local code in the imports list, as well as to reduce unnecessary usage of the barrel files.

Feature Entry Points

Each feature must have clear entry points:

  • index.ts: Client-side exports (React Client Components, hooks, utilities)
  • server.ts: Server-side exports (React Server Components, server actions)
  • node.ts: Node.js environment exports (for tests, scripts, build tools)
features/authentication/index.ts
export { LoginPage } from "./components/login-page";
export { useAuth } from "./hooks/use-auth";
features/authentication/server.ts
export { getAuthStateServer } from "./utils/get-auth-state-server";
export { PrivateLayout } from "./components/private-layout";
features/authentication/node.ts
export { AUTH_STATE_COOKIE_NAME } from "./constants";
export { parseAuthCookie } from "./utils/parse-auth-cookie";

Exporting strategy: when creating a new feature, start with re-exporting the files needed outside of it inside of index.ts using it as a barrel file. Next.js does not produce errors with files exported in this manner because it uses Turbopack, which performs tree-shaking and does not actually execute each import when importing from a barrel file. On the other hand, test environments such as Vitest or Playwright do not use bundlers, so when encountering a barrel file like index.ts they will import all re-exported code. Sometimes you will re-export files that are only supposed to be used in specific environments, such as server-only code, and hence they will fail to load in these environments. In this case, you will need to define separate barrel files for the different environments.

The most common points of issue are:

  • next/image - This component is mocked in vitest, but Playwright cannot resolve Next.js’s <Image> component when running spec files. Ensure you are not importing from a barrel file which exposes a component that directly or indirectly uses next/image.
  • server-only - Files or components marked as server-only will fail to be imported in both Vitest and Playwright. If a test file imports from a feature exposing server-only code, it’s best to separate the non-React files in a barrel called node.ts which will then be used by test files.

Feature Structure

Each feature is a self-contained module with its own components, types, and utilities. The specific directory layout is listed on the file structure page.

Public API Surface

Features expose a public API through their entry point files. Internal implementation details should not be imported directly.

// ✅ Good - Use public API
import { Resource, getResourceMetadata } from "@/features/resources";
// ❌ Bad - Import internal implementation
import { RESOURCE_METADATA } from "@/features/resources/data/resource-metadata";

Cross-Feature Dependencies

Features can depend on each other, but should do so through public APIs:

// ✅ Good
import { Bouncer } from "@/features/authentication/server";
// ❌ Bad
import { Bouncer } from "@/features/authentication/components/bouncer";

Component Organization

Component Categories

Components are organized by their role and reusability:

  1. core/: Generic, highly reusable components (buttons, links, spinners)
  2. inputs/: Form input components (text, checkbox, date, file uploads)
  3. presentation/: Display/presentational components specific to the ToPWR Admin Panel (navbar, error messages, logos)
  4. ui/: Third-party UI library components (shadcn/ui)
  5. providers/: Global context and API providers meant to wrap the entire application

Component File Structure

For simple components:

component-name.tsx
component-name.test.tsx

For complex components with multiple files:

component-name/
index.tsx # Main component export or shared code
client.tsx # Component variant for client-side use
server.tsx # Component variant for server-side use
component-name.test.tsx # Component unit tests

Component Prop Types

Define prop types inline if they are only used once. Separate them into a type or interface only if used elsewhere.

TODO: remove the @/types/components.ts file in favour of localizing component prop interfaces.


Type Definitions

Type Organization

  • Shared types: src/types/ (components.ts, helpers.ts, schemas.ts)
  • Feature types: src/features/<feature>/types/

Type Exports

Export types from feature public APIs:

features/resources/index.ts
export type { Resource } from "./enums";
export type { ResourceMetadata, ResourceFormValues } from "./types";

Route Structure

Layout Hierarchy

app/layout.tsx # Root layout
├─ (private)/layout.tsx # Private layout (auth required)
│ └─ (resources)/<resource>/layout.tsx # Resource layout
└─ (public)/layout.tsx # Public layout (no auth)

Route Permissions

Routes are protected using the Bouncer component:

export default async function PrivateLayout({ children }: WrapperProps) {
return (
<>
<Navbar />
<MainContent>
<Bouncer route="/">{children}</Bouncer>
</MainContent>
</>
);
}

Make sure to wrap any protected content using Bouncer as it ensures the currently logged in user has the appropriate permissions before rendering its children.


Environment Variables

Validation

Environment variables are validated using Zod in src/config/env.ts:

const envSchema = z.object({
NEXT_PUBLIC_API_URL: z.string().url(),
NEXT_PUBLIC_ENV: z.enum(["development", "staging", "production"]),
// ...
});
export const env = envSchema.parse(process.env);

Usage

Import validated env variables:

import { env } from "@/config/env";
const apiUrl = env.NEXT_PUBLIC_API_URL;

Naming Convention

  • Public variables: NEXT_PUBLIC_* (accessible in browser)
  • Server-only variables: No prefix (only accessible server-side)

Constants Organization

Location

  • App-wide constants: src/config/constants.ts
  • Feature-specific constants: src/features/<feature>/constants.ts

Schema Conventions

Zod Schemas

  • Shared schemas: src/schemas/
  • Resource schemas: src/features/resources/schemas/
  • Feature schemas: src/features/<feature>/schemas/

Schema Naming

Schemas use PascalCase with “Schema” suffix:

export const RequiredStringSchema = z.string().trim().min(1);
export const ColorValueSchema = z.string().regex(/^#([0-9a-fA-F]{3}){1,2}$/);
export const IsoTimestampSchema = z.string().datetime({ offset: true });

Schema Exports

Export from feature public APIs:

features/resources/index.ts
export { BannerSchema, GuideArticleSchema } from "./schemas";

Data vs Utils

src/data/

Static data and constants:

  • Error messages
  • Configuration objects
  • Predefined options/choices

src/utils/

Pure utility functions:

  • Data transformations
  • Validation helpers
  • Type guards
  • Formatting functions
data/form-error-messages.ts
// Static data
export const FORM_ERROR_MESSAGES = { ... };
utils/is-empty-value.ts
// Pure function
export const isEmptyValue = (value: unknown): boolean => { ... };