Skip to main content

Development Standards

This document establishes the coding standards and conventions for the iAm project. These standards ensure consistency, maintainability, and quality across the codebase.

General Principles

  1. Type Safety First: Strict TypeScript across the entire stack
  2. Consistency Over Cleverness: Predictable patterns over clever solutions
  3. Domain-Driven Design: Code organized around business domains
  4. Convention Over Configuration: Established patterns reduce decision fatigue
  5. Performance Awareness: Use @Measure decorators for monitoring critical paths

Critical Type System Rules

These rules are non-negotiable -- violating them breaks builds.

Never Use Date Type in TypeGraphQL

// WRONG - Breaks type generation
@Field(() => Date)
createdAt!: Date;

// CORRECT
import { GraphQLDateTime } from "graphql-scalars";

@Field((type) => GraphQLDateTime)
createdAt!: string;

Never Use Raw Model Classes in Output Types

// WRONG - Causes schema generation failures
@ObjectType()
export class SurveyResponseOutput {
@Field(() => Survey) // Raw model class
survey!: Survey;
}

// CORRECT
@ObjectType()
export class SurveyResponseOutput {
@Field(() => SurveyOutput) // Output class
survey!: SurveyOutput;
}

Always Import Types from Source

// WRONG - Duplicate type definition
interface AuthContextState {
user: any;
}

// CORRECT
import { AuthContextState } from "context/auth";

Server-Side Standards

Resource Structure

Every server resource follows this directory layout:

resources/ResourceName/
├── inputs/
│ ├── create.input.ts
│ ├── update.input.ts
│ └── query.input.ts
├── outputs/
│ ├── output.ts
│ └── query.output.ts
├── model.ts
├── resolver.ts
└── service.ts

Service Pattern

export type HandleCreateResourceArgs = {
context: Context;
input: CreateResourceInput;
};

export class ResourceService {
@Measure
static async handleCreateResource(args: HandleCreateResourceArgs) {
const { context, input } = args;
const { cypher, user, ogm, observer } = context;

// Business logic here

return result;
}
}

Requirements:

  • All service methods must be static
  • Use @Measure decorator for performance monitoring
  • Accept structured args object with context and input
  • Destructure context to extract { cypher, user, ogm, observer }

Resolver Pattern

@Resolver((of: any) => Resource)
export class ResourceResolver {
@Query((returns) => QueryResourceOutput, { nullable: true })
async queryResource(
@Arg("input") args: QueryResourceInput,
@Ctx() context: Context
): Promise<QueryResourceOutput> {
return ResourceService.handleQueryResource({
context,
input: args,
});
}
}

Neo4j OGM Patterns

// Create with relationships
const result = await ogm.model("Resource").create({
input: [
{
field1: input.field1,
owner: {
connect: { where: { node: { id: observer.id } } },
},
},
],
});

// Query with selection sets
const items = await ogm.model("Resource").find({
where: { owner: { id: observer.id } },
selectionSet: `{ id field1 owner { id alias } }`,
});

Access Control

// Always verify ownership in queries
const items = await ogm.model("Resource").find({
where: {
OR: [{ accessControl: AccessControlEnum.Public }, { owner: { id: observer.id } }],
},
});

Client-Side Standards

Component Pattern

import React from "react";
import tw from "tailwind-styled-components";

interface ComponentProps {
children?: React.ReactNode;
className?: string;
variant?: "primary" | "secondary";
}

const Container = tw.div`
flex flex-col items-center gap-4
`;

const Component: React.FC<ComponentProps> = ({ children, className = "", variant = "primary" }) => {
return <Container className={className}>{children}</Container>;
};

export default Component;

Styled Component Props

Use $ prefix for styling props that shouldn't appear in the DOM:

interface StyledButtonProps {
$variant?: "primary" | "secondary";
}

const StyledButton = tw.button<StyledButtonProps>`
px-4 py-2 rounded
${({ $variant }) =>
$variant === "primary" ? "bg-blue-500 text-white" : "bg-gray-200 text-gray-800"}
`;

Context Pattern

interface ContextState {
data: DataType[];
loading: boolean;
}

interface ContextActions {
loadData: () => Promise<void>;
}

type ContextType = ContextState & ContextActions;

const MyContext = createContext<ContextType | null>(null);

export const useMyContext = (): ContextType => {
const context = useContext(MyContext);
if (!context) {
throw new Error("useMyContext must be used within MyContextProvider");
}
return context;
};

GraphQL Naming Conventions

TypeConventionExample
Queriesquery{ResourceName}queryUser, querySessions
Mutations{action}{ResourceName}createUser, updateSession
Create InputsCreate{Resource}InputCreateSessionInput
Query InputsQuery{Resource}InputQuerySessionInput
Outputs{Resource}OutputSessionOutput
Query OutputsQuery{Resource}OutputQuerySessionOutput

File Naming Conventions

ContextConventionExample
Server resourcesPascalCase directoriesSession/Type/Version/
Server filescamelCaseservice.ts, resolver.ts
Client componentsPascalCase directories/filesSessionCard/index.tsx
Client pagescamelCase directoriessessions/, explorer/
ConstantsSCREAMING_SNAKE_CASEMAX_SESSION_DURATION

Testing

Component Tests (Vitest)

import { render, screen } from "@testing-library/react";

describe("ComponentName", () => {
it("renders with default props", () => {
render(<ComponentName>Test</ComponentName>);
expect(screen.getByText("Test")).toBeInTheDocument();
});
});

Mock Modules Correctly

vi.mock("context/auth", async () => {
const actual = await vi.importActual<typeof import("context/auth")>("context/auth");
return {
...actual, // Keep all types
useAuth: vi.fn(), // Mock only implementation
};
});

Package Management

  • Always use yarn, never npm
  • Never use as any without explicit permission
  • Always check for existing components before creating new ones
  • For the frontend, only use MUI components if explicitly asked
  • Run cd server && yarn build:types:all after any schema changes