Skip to main content

Development Overview

This guide covers the development setup, coding standards, and best practices for contributing to the Akako LMS project.

Development Environment Setup

Prerequisites

  • Node.js: 18.0 or higher (20.0+ recommended)
  • pnpm: Package manager (recommended over npm)
  • PostgreSQL: 13.0 or higher
  • Git: Version control
  • VS Code: Recommended IDE with extensions

Quick Start

  1. Clone Repository:

    git clone https://github.com/quivlabs/lms.git
    cd lms
  2. Install Dependencies:

    pnpm install
  3. Environment Setup:

    cp .env.example .env.local
    # Edit .env.local with your configuration
  4. Database Setup:

    pnpm prisma generate
    pnpm prisma db push
  5. Start Development Server:

    pnpm dev

Visit http://localhost:3000 to see the application.

Project Structure

LMS/
├── app/ # Next.js App Router
│ ├── (pages)/ # Page routes
│ │ ├── admin/ # Admin pages
│ │ ├── mentor/ # Mentor pages
│ │ ├── learner/ # Learner pages
│ │ └── auth/ # Authentication pages
│ ├── api/ # API routes
│ │ ├── admin/ # Admin API endpoints
│ │ ├── auth/ # Authentication endpoints
│ │ ├── education/ # Education system endpoints
│ │ └── s3/ # File management endpoints
│ ├── globals.css # Global styles
│ └── layout.tsx # Root layout
├── components/ # Reusable components
│ ├── ui/ # Basic UI components
│ ├── forms/ # Form components
│ ├── modals/ # Modal components
│ └── layout/ # Layout components
├── lib/ # Utility libraries
│ ├── auth.ts # Authentication utilities
│ ├── prisma.ts # Database client
│ ├── rbac.ts # Role-based access control
│ ├── email.ts # Email service
│ └── hooks/ # Custom React hooks
├── prisma/ # Database schema and migrations
│ ├── schema.prisma # Database schema
│ └── migrations/ # Database migrations
├── public/ # Static assets
│ ├── images/ # Image assets
│ └── css/ # Additional CSS files
├── scripts/ # Build and utility scripts
├── docs/ # Documentation
└── package.json # Dependencies and scripts

Technology Stack

Core Technologies

  • Next.js 14: React framework with App Router
  • TypeScript: Type-safe JavaScript
  • TailwindCSS: Utility-first CSS framework
  • Prisma: Database ORM and query builder
  • PostgreSQL: Primary database
  • Clerk.js: Authentication service

State Management

  • Jotai: Lightweight state management
  • TanStack Query: Server state management
  • React Hook Form: Form state management

Development Tools

  • ESLint: Code linting
  • Prettier: Code formatting
  • Husky: Git hooks
  • Commitlint: Commit message linting
  • TypeScript: Type checking

Coding Standards

TypeScript Guidelines

Type Definitions

// Use interfaces for object shapes
interface User {
id: string;
email: string;
firstName?: string;
lastName?: string;
}

// Use types for unions and computed types
type UserRole = 'LEARNER' | 'MENTOR' | 'ADMIN';
type UserWithRoles = User & { roles: UserRole[] };

// Use enums for constants
enum UserStatus {
ACTIVE = 'ACTIVE',
INACTIVE = 'INACTIVE',
SUSPENDED = 'SUSPENDED',
}

Function Definitions

// Use explicit return types for public functions
export async function getUserById(id: string): Promise<User | null> {
return await prisma.user.findUnique({
where: { id },
});
}

// Use arrow functions for simple operations
const formatUserName = (user: User): string => {
return `${user.firstName} ${user.lastName}`.trim();
};

Error Handling

// Use Result pattern for operations that can fail
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };

export async function createUser(userData: CreateUserData): Promise<Result<User>> {
try {
const user = await prisma.user.create({
data: userData,
});
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}

React Component Guidelines

Component Structure

// Use named exports
export function UserCard({ user, onEdit }: UserCardProps) {
// Hooks at the top
const [isEditing, setIsEditing] = useState(false);
const { mutate: updateUser } = useMutation(updateUserMutation);

// Event handlers
const handleEdit = useCallback(() => {
setIsEditing(true);
}, []);

const handleSave = useCallback(async (data: UserFormData) => {
await updateUser({ id: user.id, data });
setIsEditing(false);
}, [user.id, updateUser]);

// Render
return (
<div className="user-card">
{/* Component JSX */}
</div>
);
}

// Props interface
interface UserCardProps {
user: User;
onEdit?: (user: User) => void;
}

Custom Hooks

// Use custom hooks for reusable logic
export function useUser(userId: string) {
return useQuery({
queryKey: ['user', userId],
queryFn: () => getUserById(userId),
enabled: !!userId,
});
}

// Use proper dependency arrays
export function useUserRoles(userId: string) {
const { data: user } = useUser(userId);

return useMemo(() => {
return user?.userRoles?.map(ur => ur.role) || [];
}, [user?.userRoles]);
}

API Route Guidelines

Route Structure

// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRoleGuard } from '@/lib/rbac';
import { Role } from '@prisma/client';

// GET /api/users/[id]
export const GET = withRoleGuard(
async (req: NextRequest, { params }: { params: { id: string } }) => {
try {
const user = await getUserById(params.id);

if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}

return NextResponse.json({ success: true, data: user });
} catch (error) {
console.error('Error fetching user:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
{ roles: [Role.ADMIN] }
);

// PUT /api/users/[id]
export const PUT = withRoleGuard(
async (req: NextRequest, { params }: { params: { id: string } }) => {
try {
const data = await req.json();
const user = await updateUser(params.id, data);

return NextResponse.json({ success: true, data: user });
} catch (error) {
console.error('Error updating user:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
},
{ roles: [Role.ADMIN] }
);

Error Handling

// Centralized error handling
export class APIError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string
) {
super(message);
this.name = 'APIError';
}
}

// Error handler utility
export function handleAPIError(error: unknown): NextResponse {
if (error instanceof APIError) {
return NextResponse.json(
{ error: error.message, code: error.code },
{ status: error.statusCode }
);
}

console.error('Unexpected error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}

Database Guidelines

Prisma Schema

// Use descriptive model names
model User {
id String @id @default(cuid())
clerkId String @unique
email String @unique

// Use proper field types
firstName String?
lastName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

// Use proper relationships
userRoles UserRole[]

// Use proper constraints
@@map("users")
}

// Use enums for fixed values
enum Role {
LEARNER
MENTOR
ADMIN
}

// Use proper indexes
model UserRole {
id String @id @default(cuid())
userId String
role Role

@@index([userId])
@@index([role])
@@map("user_roles")
}

Query Patterns

// Use select for performance
export async function getUsersList() {
return await prisma.user.findMany({
select: {
id: true,
email: true,
firstName: true,
lastName: true,
userRoles: {
select: {
role: true,
},
},
},
});
}

// Use transactions for complex operations
export async function assignRoleToUser(userId: string, role: Role, adminId: string) {
return await prisma.$transaction(async (tx) => {
// Deactivate existing role
await tx.userRole.updateMany({
where: {
userId,
role,
isActive: true,
},
data: {
isActive: false,
effectiveEndDate: new Date(),
},
});

// Create new role assignment
return await tx.userRole.create({
data: {
userId,
role,
assignedBy: adminId,
effectiveStartDate: new Date(),
},
});
});
}

Testing Guidelines

Unit Testing

// Use Jest for unit tests
import { describe, it, expect, jest } from '@jest/globals';
import { getUserById } from '@/lib/users';

describe('getUserById', () => {
it('should return user when found', async () => {
const mockUser = { id: '1', email: 'test@example.com' };
jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(mockUser);

const result = await getUserById('1');

expect(result).toEqual(mockUser);
expect(prisma.user.findUnique).toHaveBeenCalledWith({
where: { id: '1' },
});
});

it('should return null when user not found', async () => {
jest.spyOn(prisma.user, 'findUnique').mockResolvedValue(null);

const result = await getUserById('999');

expect(result).toBeNull();
});
});

Integration Testing

// Use Playwright for integration tests
import { test, expect } from '@playwright/test';

test('user can login and view dashboard', async ({ page }) => {
await page.goto('/login');

await page.fill('[data-testid="email"]', 'test@example.com');
await page.fill('[data-testid="password"]', 'password123');
await page.click('[data-testid="login-button"]');

await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toBeVisible();
});

API Testing

// Use Supertest for API testing
import request from 'supertest';
import { app } from '@/app';

describe('GET /api/users', () => {
it('should return users list for admin', async () => {
const response = await request(app)
.get('/api/users')
.set('Authorization', 'Bearer admin-token')
.expect(200);

expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});

it('should return 403 for non-admin user', async () => {
await request(app)
.get('/api/users')
.set('Authorization', 'Bearer user-token')
.expect(403);
});
});

Git Workflow

Branch Naming

  • feature/feature-name: New features
  • bugfix/bug-description: Bug fixes
  • hotfix/critical-fix: Critical production fixes
  • refactor/refactor-description: Code refactoring
  • docs/documentation-update: Documentation updates

Commit Messages

Use conventional commits:

feat: add user role management system
fix: resolve authentication token expiration issue
docs: update API documentation
refactor: simplify user profile component
test: add unit tests for user service
chore: update dependencies

Pull Request Process

  1. Create Feature Branch:

    git checkout -b feature/user-role-management
  2. Make Changes:

    • Write code following guidelines
    • Add tests
    • Update documentation
  3. Commit Changes:

    git add .
    git commit -m "feat: add user role management system"
  4. Push and Create PR:

    git push origin feature/user-role-management
    # Create PR on GitHub
  5. Code Review:

    • Address review comments
    • Update tests if needed
    • Ensure CI passes
  6. Merge:

    • Squash and merge
    • Delete feature branch

Performance Guidelines

Code Splitting

// Use dynamic imports for large components
const UserManagementModal = dynamic(
() => import('@/components/modals/UserManagementModal'),
{ loading: () => <div>Loading...</div> }
);

// Use React.lazy for route-based splitting
const AdminDashboard = lazy(() => import('@/app/admin/dashboard/page'));

Memoization

// Use React.memo for expensive components
export const UserCard = React.memo(({ user, onEdit }: UserCardProps) => {
return (
<div className="user-card">
{/* Component content */}
</div>
);
});

// Use useMemo for expensive calculations
const expensiveValue = useMemo(() => {
return users.reduce((acc, user) => acc + user.score, 0);
}, [users]);

Database Optimization

// Use proper indexing
// Add indexes in Prisma schema
model User {
id String @id @default(cuid())
email String @unique
clerkId String @unique

@@index([email])
@@index([clerkId])
}

// Use pagination for large datasets
export async function getUsers(page = 1, limit = 20) {
const skip = (page - 1) * limit;

return await prisma.user.findMany({
skip,
take: limit,
orderBy: { createdAt: 'desc' },
});
}

Security Guidelines

Input Validation

// Use Zod for schema validation
import { z } from 'zod';

const CreateUserSchema = z.object({
email: z.string().email(),
firstName: z.string().min(1).max(50),
lastName: z.string().min(1).max(50),
});

export async function createUser(data: unknown) {
const validatedData = CreateUserSchema.parse(data);
// Use validated data
}

Authentication

// Always validate authentication server-side
export async function getCurrentUser() {
const { userId } = await auth();

if (!userId) {
throw new Error('Unauthorized');
}

return await prisma.user.findUnique({
where: { clerkId: userId },
});
}

Authorization

// Use role guards for API protection
export const GET = withRoleGuard(
async (req: NextRequest) => {
// Protected logic here
},
{ roles: [Role.ADMIN] }
);

Debugging

Development Tools

VS Code Extensions

  • ES7+ React/Redux/React-Native snippets
  • Tailwind CSS IntelliSense
  • Prisma
  • TypeScript Importer
  • GitLens

Debug Configuration

// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"name": "Next.js: debug server-side",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/next",
"args": ["dev"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
},
{
"name": "Next.js: debug client-side",
"type": "chrome",
"request": "launch",
"url": "http://localhost:3000"
}
]
}

Logging

// Use structured logging
import { createLogger } from 'winston';

const logger = createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'error.log', level: 'error' }),
],
});

// Use in code
logger.info('User created', { userId: user.id, email: user.email });
logger.error('Database error', { error: error.message, query: 'getUserById' });

Documentation

Code Documentation

/**
* Creates a new user with the provided data
* @param userData - User data including email, firstName, lastName
* @returns Promise resolving to the created user
* @throws {ValidationError} When userData is invalid
* @throws {DatabaseError} When database operation fails
* @example
* ```typescript
* const user = await createUser({
* email: 'john@example.com',
* firstName: 'John',
* lastName: 'Doe'
* });
* ```
*/
export async function createUser(userData: CreateUserData): Promise<User> {
// Implementation
}

API Documentation

/**
* @swagger
* /api/users:
* get:
* summary: Get all users
* tags: [Users]
* security:
* - bearerAuth: []
* responses:
* 200:
* description: List of users
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/

Contributing

Getting Started

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests
  5. Update documentation
  6. Submit a pull request

Code Review Checklist

  • Code follows TypeScript guidelines
  • Components are properly typed
  • API routes have proper error handling
  • Database queries are optimized
  • Tests are included
  • Documentation is updated
  • Security considerations addressed
  • Performance implications considered

Next Steps