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 rootimport { Resource, getResourceMetadata } from "@/features/resources";import { AbstractResourceForm } from "@/features/abstract-resource-form";
// ✅ Good - Import from feature entry point filesimport { getAuthStateServer } from "@/features/authentication/server";import { getResourceMetadata } from "@/features/resources/node";
// ❌ Bad - Deep imports into feature internalsimport 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)
export { LoginPage } from "./components/login-page";export { useAuth } from "./hooks/use-auth";export { getAuthStateServer } from "./utils/get-auth-state-server";export { PrivateLayout } from "./components/private-layout";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 usesnext/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 callednode.tswhich 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 APIimport { Resource, getResourceMetadata } from "@/features/resources";
// ❌ Bad - Import internal implementationimport { RESOURCE_METADATA } from "@/features/resources/data/resource-metadata";Cross-Feature Dependencies
Features can depend on each other, but should do so through public APIs:
// ✅ Goodimport { Bouncer } from "@/features/authentication/server";
// ❌ Badimport { Bouncer } from "@/features/authentication/components/bouncer";Component Organization
Component Categories
Components are organized by their role and reusability:
core/: Generic, highly reusable components (buttons, links, spinners)inputs/: Form input components (text, checkbox, date, file uploads)presentation/: Display/presentational components specific to the ToPWR Admin Panel (navbar, error messages, logos)ui/: Third-party UI library components (shadcn/ui)providers/: Global context and API providers meant to wrap the entire application
Component File Structure
For simple components:
component-name.tsxcomponent-name.test.tsxFor 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 testsComponent 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:
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:
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
// Static dataexport const FORM_ERROR_MESSAGES = { ... };// Pure functionexport const isEmptyValue = (value: unknown): boolean => { ... };