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
-
Clone Repository:
git clone https://github.com/quivlabs/lms.git
cd lms -
Install Dependencies:
pnpm install -
Environment Setup:
cp .env.example .env.local
# Edit .env.local with your configuration -
Database Setup:
pnpm prisma generate
pnpm prisma db push -
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 featuresbugfix/bug-description: Bug fixeshotfix/critical-fix: Critical production fixesrefactor/refactor-description: Code refactoringdocs/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
-
Create Feature Branch:
git checkout -b feature/user-role-management -
Make Changes:
- Write code following guidelines
- Add tests
- Update documentation
-
Commit Changes:
git add .
git commit -m "feat: add user role management system" -
Push and Create PR:
git push origin feature/user-role-management
# Create PR on GitHub -
Code Review:
- Address review comments
- Update tests if needed
- Ensure CI passes
-
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
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests
- Update documentation
- 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
- Environment Setup: Detailed development environment setup
- Testing Guide: Comprehensive testing strategies
- API Development: API development best practices
- Component Library: Reusable component guidelines