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
- Type Safety First: Strict TypeScript across the entire stack
- Consistency Over Cleverness: Predictable patterns over clever solutions
- Domain-Driven Design: Code organized around business domains
- Convention Over Configuration: Established patterns reduce decision fatigue
- Performance Awareness: Use
@Measuredecorators 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
@Measuredecorator for performance monitoring - Accept structured args object with
contextandinput - 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
| Type | Convention | Example |
|---|---|---|
| Queries | query{ResourceName} | queryUser, querySessions |
| Mutations | {action}{ResourceName} | createUser, updateSession |
| Create Inputs | Create{Resource}Input | CreateSessionInput |
| Query Inputs | Query{Resource}Input | QuerySessionInput |
| Outputs | {Resource}Output | SessionOutput |
| Query Outputs | Query{Resource}Output | QuerySessionOutput |
File Naming Conventions
| Context | Convention | Example |
|---|---|---|
| Server resources | PascalCase directories | Session/Type/Version/ |
| Server files | camelCase | service.ts, resolver.ts |
| Client components | PascalCase directories/files | SessionCard/index.tsx |
| Client pages | camelCase directories | sessions/, explorer/ |
| Constants | SCREAMING_SNAKE_CASE | MAX_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 anywithout 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:allafter any schema changes