Code Guidelines
This document defines the standards and best practices to follow when developing and contributing to the TrioSigno project.
General Principles
We follow these fundamental principles:
- Readability - Code should be easy to read and understand
- Maintainability - Code should be easy to maintain and evolve
- Testability - Code should be easy to test
- Performance - Code should be efficient and optimized
- Security - Code should follow security best practices
Project Structure
The project is structured according to a modular architecture:
triosigno/
├── client/ # Frontend React/Next.js
│ ├── components/ # Reusable UI components
│ ├── hooks/ # Custom React hooks
│ ├── pages/ # Application pages
│ ├── public/ # Static files
│ ├── styles/ # Global styles and themes
│ └── utils/ # Frontend utilities
│
├── server/ # Backend Node.js/Express
│ ├── api/ # API routes and controllers
│ ├── config/ # Server configuration
│ ├── middleware/ # Express middleware
│ ├── models/ # Data models
│ ├── services/ # Business services
│ └── utils/ # Backend utilities
│
├── shared/ # Shared frontend/backend code
│ ├── constants/ # Shared constants
│ ├── types/ # Shared TypeScript types
│ └── validation/ # Validation schemas
│
├── ia/ # Artificial Intelligence components
│ ├── models/ # Trained AI models
│ ├── training/ # Training scripts
│ └── utils/ # AI utilities
│
├── prisma/ # Prisma schema and migrations
│
├── tests/ # Global tests
│ ├── e2e/ # End-to-end tests
│ └── integration/ # Integration tests
│
└── scripts/ # Utility scripts
Code Standards
TypeScript
We use TypeScript to benefit from static typing:
// Prefer interfaces for data structures
interface User {
id: string;
username: string;
email: string;
role: UserRole;
createdAt: Date;
}
// Use types for unions and utility types
type UserRole = "admin" | "teacher" | "student";
type UserWithoutId = Omit<User, "id">;
// Avoid any, prefer unknown if necessary
function processData(data: unknown): void {
if (isUser(data)) {
// Type is now User
console.log(data.username);
}
}
// Type guard function
function isUser(obj: unknown): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
"id" in obj &&
"username" in obj &&
"email" in obj
);
}
Naming
We follow these naming conventions:
- PascalCase for classes, interfaces, types, and React components
- camelCase for variables, functions, and properties
- UPPER_CASE for constants
- kebab-case for CSS files and URLs
// Constants
const MAX_RETRY_ATTEMPTS = 3;
// Variables
const userProfile = getUserProfile();
// Functions
function calculateProgress(userId: string): number {
// ...
}
// Classes
class AuthenticationService {
// ...
}
// Interfaces
interface SignProperties {
// ...
}
// React Components
function UserProfileCard({ user }: UserProfileCardProps) {
// ...
}
Formatting
We use ESLint and Prettier to ensure code consistency. Configuration files are included in the project.
ESLint configuration:
{
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "react", "react-hooks"],
"rules": {
"no-console": ["warn", { "allow": ["warn", "error"] }],
"react/prop-types": "off",
"react/react-in-jsx-scope": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ "argsIgnorePattern": "^_" }
]
}
}
Prettier configuration:
{
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2
}
Comments
We follow these practices for comments:
- JSDoc comments for public functions and classes
- Simple comments to explain complex logic
- Avoid comments that simply repeat the code
/**
* Calculates the mastery level of a sign for a user.
*
* @param userId - The user's ID
* @param signId - The sign's ID
* @returns The mastery level between 0 and 100
*/
async function calculateMasteryLevel(
userId: string,
signId: string
): Promise<number> {
// Get attempt history
const attempts = await getAttemptHistory(userId, signId);
// Use a weighted average of the last 10 attempts
// where recent attempts have more weight
const recentAttempts = attempts.slice(-10);
// ...complex calculation...
return masteryLevel;
}
Frontend
React Components
We use functional components with hooks:
import React, { useState, useEffect } from "react";
import { useUser } from "@/hooks/useUser";
import { Button } from "@/components/ui/Button";
interface LessonCardProps {
lessonId: string;
title: string;
description: string;
onStart: () => void;
}
export function LessonCard({
lessonId,
title,
description,
onStart,
}: LessonCardProps) {
const { user } = useUser();
const [progress, setProgress] = useState(0);
useEffect(() => {
// Load user progress for this lesson
async function loadProgress() {
if (user) {
const userProgress = await fetchLessonProgress(user.id, lessonId);
setProgress(userProgress);
}
}
loadProgress();
}, [user, lessonId]);
return (
<div className="lesson-card">
<h3>{title}</h3>
<p>{description}</p>
<div className="progress-bar" style={{ width: `${progress}%` }} />
<Button onClick={onStart}>Start</Button>
</div>
);
}
State Management
We use a combination of React hooks (useState, useReducer) and React Context for state management:
// Context for authentication management
import React, { createContext, useContext, useReducer, ReactNode } from "react";
interface AuthState {
isAuthenticated: boolean;
user: User | null;
loading: boolean;
error: string | null;
}
type AuthAction =
| { type: "LOGIN_START" }
| { type: "LOGIN_SUCCESS"; payload: User }
| { type: "LOGIN_FAILURE"; payload: string }
| { type: "LOGOUT" };
const AuthContext = createContext<
| {
state: AuthState;
dispatch: React.Dispatch<AuthAction>;
}
| undefined
>(undefined);
function authReducer(state: AuthState, action: AuthAction): AuthState {
switch (action.type) {
case "LOGIN_START":
return { ...state, loading: true, error: null };
case "LOGIN_SUCCESS":
return {
...state,
isAuthenticated: true,
user: action.payload,
loading: false,
error: null,
};
case "LOGIN_FAILURE":
return {
...state,
isAuthenticated: false,
user: null,
loading: false,
error: action.payload,
};
case "LOGOUT":
return {
...state,
isAuthenticated: false,
user: null,
loading: false,
error: null,
};
default:
return state;
}
}
export function AuthProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(authReducer, {
isAuthenticated: false,
user: null,
loading: false,
error: null,
});
return (
<AuthContext.Provider value={{ state, dispatch }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}
Styles
We use a combination of CSS Modules and Tailwind CSS:
// Example with CSS Modules
import styles from "./Button.module.css";
export function Button({ children, variant = "primary", ...props }) {
return (
<button className={`${styles.button} ${styles[variant]}`} {...props}>
{children}
</button>
);
}
// Example with Tailwind CSS
export function Card({ title, children }) {
return (
<div className="rounded-lg shadow-md p-4 bg-white dark:bg-gray-800">
<h3 className="text-xl font-semibold mb-2">{title}</h3>
<div>{children}</div>
</div>
);
}
Backend
Architecture
We follow a layered architecture:
- Routes - Define API endpoints
- Controllers - Handle requests and responses
- Services - Contain business logic
- Models - Define data structure
// Route
import express from "express";
import { getLessons, getLessonById } from "../controllers/lessonController";
import { authenticate } from "../middleware/auth";
const router = express.Router();
router.get("/lessons", authenticate, getLessons);
router.get("/lessons/:id", authenticate, getLessonById);
export default router;
// Controller
import { Request, Response } from "express";
import { LessonService } from "../services/lessonService";
const lessonService = new LessonService();
export async function getLessons(req: Request, res: Response) {
try {
const lessons = await lessonService.getAllLessons();
res.json(lessons);
} catch (error) {
res.status(500).json({ message: "Failed to fetch lessons" });
}
}
// Service
import { prisma } from "../config/database";
export class LessonService {
async getAllLessons() {
return prisma.lesson.findMany({
orderBy: { order: "asc" },
include: {
module: true,
},
});
}
}
Error Handling
We use a centralized error handling middleware:
import { Request, Response, NextFunction } from "express";
// Custom error types
export class AppError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404);
}
}
export class UnauthorizedError extends AppError {
constructor(message = "Unauthorized") {
super(message, 401);
}
}
// Error handling middleware
export function errorHandler(
err: Error,
req: Request,
res: Response,
_next: NextFunction
) {
console.error(err);
if (err instanceof AppError) {
return res.status(err.statusCode).json({
status: "error",
message: err.message,
});
}
return res.status(500).json({
status: "error",
message: "Internal server error",
});
}
Validation
We use Zod for data validation:
import { z } from "zod";
import { Request, Response, NextFunction } from "express";
// Validation schema for user creation
const createUserSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
password: z.string().min(8),
firstName: z.string().optional(),
lastName: z.string().optional(),
});
// Validation middleware
export function validateCreateUser(
req: Request,
res: Response,
next: NextFunction
) {
try {
createUserSchema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
status: "error",
message: "Validation failed",
errors: error.errors,
});
}
next(error);
}
}
Testing
Unit Tests
We use Jest for unit testing:
import { calculateMasteryLevel } from "./masteryCalculator";
import { getAttemptHistory } from "./attemptService";
// Mock dependencies
jest.mock("./attemptService", () => ({
getAttemptHistory: jest.fn(),
}));
describe("calculateMasteryLevel", () => {
beforeEach(() => {
jest.clearAllMocks();
});
test("should return 0 for no attempts", async () => {
(getAttemptHistory as jest.Mock).mockResolvedValue([]);
const result = await calculateMasteryLevel("user1", "sign1");
expect(result).toBe(0);
expect(getAttemptHistory).toHaveBeenCalledWith("user1", "sign1");
});
test("should calculate mastery based on recent attempts", async () => {
// Configure mock to return test data
(getAttemptHistory as jest.Mock).mockResolvedValue([
{ correct: true, date: new Date("2023-01-01") },
{ correct: false, date: new Date("2023-01-02") },
{ correct: true, date: new Date("2023-01-03") },
]);
const result = await calculateMasteryLevel("user1", "sign1");
// Verify that the result matches what we expect
expect(result).toBeGreaterThan(0);
expect(result).toBeLessThanOrEqual(100);
});
});
Integration Tests
We use Supertest to test APIs:
import request from "supertest";
import { app } from "../app";
import { prisma } from "../config/database";
import { createUser, generateAuthToken } from "../utils/testHelpers";
describe("Lesson API", () => {
let authToken: string;
beforeAll(async () => {
// Create a test user and generate a token
const user = await createUser({
username: "testuser",
email: "test@example.com",
password: "password123",
});
authToken = generateAuthToken(user);
});
afterAll(async () => {
// Clean up the database
await prisma.user.deleteMany();
await prisma.$disconnect();
});
test("GET /api/lessons should return all lessons", async () => {
const response = await request(app)
.get("/api/lessons")
.set("Authorization", `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body).toBeInstanceOf(Array);
});
test("GET /api/lessons/:id should return a specific lesson", async () => {
// Create a test lesson
const lesson = await prisma.lesson.create({
data: {
title: "Test Lesson",
description: "Test description",
order: 1,
module: {
create: {
title: "Test Module",
description: "Test module description",
order: 1,
},
},
},
});
const response = await request(app)
.get(`/api/lessons/${lesson.id}`)
.set("Authorization", `Bearer ${authToken}`);
expect(response.status).toBe(200);
expect(response.body.id).toBe(lesson.id);
expect(response.body.title).toBe("Test Lesson");
});
});
End-to-End Tests
We use Playwright for end-to-end tests:
import { test, expect } from "@playwright/test";
test.describe("User Authentication Flow", () => {
test("should allow user to sign up, log in, and access protected content", async ({
page,
}) => {
// 1. Navigate to the signup page
await page.goto("/signup");
// 2. Fill out and submit the signup form
await page.fill('input[name="username"]', `testuser-${Date.now()}`);
await page.fill('input[name="email"]', `test-${Date.now()}@example.com`);
await page.fill('input[name="password"]', "Password123!");
await page.fill('input[name="confirmPassword"]', "Password123!");
await page.click('button[type="submit"]');
// 3. Verify redirect to login page or dashboard
await expect(page).toHaveURL(/login|dashboard/);
// 4. If redirected to login, complete login
if (page.url().includes("login")) {
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', "Password123!");
await page.click('button[type="submit"]');
}
// 5. Verify access to dashboard
await expect(page).toHaveURL(/dashboard/);
await expect(page.locator("h1")).toContainText("Dashboard");
// 6. Navigate to lessons page
await page.click("text=Lessons");
// 7. Verify access to lessons
await expect(page).toHaveURL(/lessons/);
await expect(page.locator("h1")).toContainText("Lessons");
});
});
Security
Authentication
We use JWT (JSON Web Tokens) for authentication:
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import { prisma } from "../config/database";
import { UnauthorizedError } from "../utils/errors";
export class AuthService {
async login(email: string, password: string) {
// Find user by email
const user = await prisma.user.findUnique({ where: { email } });
if (!user) {
throw new UnauthorizedError("Invalid credentials");
}
// Verify password
const validPassword = await bcrypt.compare(password, user.password);
if (!validPassword) {
throw new UnauthorizedError("Invalid credentials");
}
// Generate JWT token
const token = jwt.sign(
{
id: user.id,
role: user.role,
},
process.env.JWT_SECRET!,
{ expiresIn: "7d" }
);
return {
token,
user: {
id: user.id,
email: user.email,
username: user.username,
role: user.role,
},
};
}
}
Input Validation
We validate all user inputs:
import { z } from "zod";
// Validation schema for query parameters
const lessonQuerySchema = z.object({
moduleId: z.string().uuid().optional(),
limit: z.coerce.number().min(1).max(100).optional().default(20),
page: z.coerce.number().min(1).optional().default(1),
});
// Validate and extract parameters
export function validateLessonQuery(query: any) {
return lessonQuerySchema.parse(query);
}
Protection Against Common Attacks
We implement protections against common attacks:
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import xss from "xss-clean";
import hpp from "hpp";
export function configureSecurityMiddleware(app) {
// HTTP header protection
app.use(helmet());
// Rate limiting to prevent brute force attacks
app.use(
"/api/auth",
rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 requests per IP
message: "Too many login attempts, please try again later",
})
);
// XSS attack protection
app.use(xss());
// HTTP parameter pollution protection
app.use(hpp());
}
Development Process
Git Workflow
We use the GitFlow model:
- main - Stable production code
- develop - Main development branch
- feature/* - Branches for new features
- bugfix/* - Branches for bug fixes
- release/* - Branches for version preparation
Pull Request Process
- Create a branch from develop
- Develop the feature or fix the bug
- Run tests locally
- Create a Pull Request to develop
- Wait for code review and CI tests execution
- Merge the Pull Request once approved
Commit Messages
We follow the Conventional Commits format:
type(scope): concise description
Detailed description if necessary.
Issue references: #123, #456
Common types:
- feat - New feature
- fix - Bug fix
- docs - Documentation
- style - Formatting, missing semicolons, etc.
- refactor - Code refactoring
- test - Tests
- chore - Maintenance tasks
Versioning
We follow Semantic Versioning (SemVer):
- MAJOR - Changes incompatible with previous versions
- MINOR - Backward-compatible feature additions
- PATCH - Backward-compatible bug fixes