diff --git a/Backend/.agent/skills/nodejs-backend-patterns b/Backend/.agent/skills/nodejs-backend-patterns new file mode 120000 index 00000000..1c65884f --- /dev/null +++ b/Backend/.agent/skills/nodejs-backend-patterns @@ -0,0 +1 @@ +../../.agents/skills/nodejs-backend-patterns \ No newline at end of file diff --git a/Backend/.agents/skills/nodejs-backend-patterns/SKILL.md b/Backend/.agents/skills/nodejs-backend-patterns/SKILL.md new file mode 100644 index 00000000..70ab552f --- /dev/null +++ b/Backend/.agents/skills/nodejs-backend-patterns/SKILL.md @@ -0,0 +1,1048 @@ +--- +name: nodejs-backend-patterns +description: Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures. +--- + +# Node.js Backend Patterns + +Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices. + +## When to Use This Skill + +- Building REST APIs or GraphQL servers +- Creating microservices with Node.js +- Implementing authentication and authorization +- Designing scalable backend architectures +- Setting up middleware and error handling +- Integrating databases (SQL and NoSQL) +- Building real-time applications with WebSockets +- Implementing background job processing + +## Core Frameworks + +### Express.js - Minimalist Framework + +**Basic Setup:** + +```typescript +import express, { Request, Response, NextFunction } from "express"; +import helmet from "helmet"; +import cors from "cors"; +import compression from "compression"; + +const app = express(); + +// Security middleware +app.use(helmet()); +app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") })); +app.use(compression()); + +// Body parsing +app.use(express.json({ limit: "10mb" })); +app.use(express.urlencoded({ extended: true, limit: "10mb" })); + +// Request logging +app.use((req: Request, res: Response, next: NextFunction) => { + console.log(`${req.method} ${req.path}`); + next(); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); +}); +``` + +### Fastify - High Performance Framework + +**Basic Setup:** + +```typescript +import Fastify from "fastify"; +import helmet from "@fastify/helmet"; +import cors from "@fastify/cors"; +import compress from "@fastify/compress"; + +const fastify = Fastify({ + logger: { + level: process.env.LOG_LEVEL || "info", + transport: { + target: "pino-pretty", + options: { colorize: true }, + }, + }, +}); + +// Plugins +await fastify.register(helmet); +await fastify.register(cors, { origin: true }); +await fastify.register(compress); + +// Type-safe routes with schema validation +fastify.post<{ + Body: { name: string; email: string }; + Reply: { id: string; name: string }; +}>( + "/users", + { + schema: { + body: { + type: "object", + required: ["name", "email"], + properties: { + name: { type: "string", minLength: 1 }, + email: { type: "string", format: "email" }, + }, + }, + }, + }, + async (request, reply) => { + const { name, email } = request.body; + return { id: "123", name }; + }, +); + +await fastify.listen({ port: 3000, host: "0.0.0.0" }); +``` + +## Architectural Patterns + +### Pattern 1: Layered Architecture + +**Structure:** + +``` +src/ +├── controllers/ # Handle HTTP requests/responses +├── services/ # Business logic +├── repositories/ # Data access layer +├── models/ # Data models +├── middleware/ # Express/Fastify middleware +├── routes/ # Route definitions +├── utils/ # Helper functions +├── config/ # Configuration +└── types/ # TypeScript types +``` + +**Controller Layer:** + +```typescript +// controllers/user.controller.ts +import { Request, Response, NextFunction } from "express"; +import { UserService } from "../services/user.service"; +import { CreateUserDTO, UpdateUserDTO } from "../types/user.types"; + +export class UserController { + constructor(private userService: UserService) {} + + async createUser(req: Request, res: Response, next: NextFunction) { + try { + const userData: CreateUserDTO = req.body; + const user = await this.userService.createUser(userData); + res.status(201).json(user); + } catch (error) { + next(error); + } + } + + async getUser(req: Request, res: Response, next: NextFunction) { + try { + const { id } = req.params; + const user = await this.userService.getUserById(id); + res.json(user); + } catch (error) { + next(error); + } + } + + async updateUser(req: Request, res: Response, next: NextFunction) { + try { + const { id } = req.params; + const updates: UpdateUserDTO = req.body; + const user = await this.userService.updateUser(id, updates); + res.json(user); + } catch (error) { + next(error); + } + } + + async deleteUser(req: Request, res: Response, next: NextFunction) { + try { + const { id } = req.params; + await this.userService.deleteUser(id); + res.status(204).send(); + } catch (error) { + next(error); + } + } +} +``` + +**Service Layer:** + +```typescript +// services/user.service.ts +import { UserRepository } from "../repositories/user.repository"; +import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types"; +import { NotFoundError, ValidationError } from "../utils/errors"; +import bcrypt from "bcrypt"; + +export class UserService { + constructor(private userRepository: UserRepository) {} + + async createUser(userData: CreateUserDTO): Promise { + // Validation + const existingUser = await this.userRepository.findByEmail(userData.email); + if (existingUser) { + throw new ValidationError("Email already exists"); + } + + // Hash password + const hashedPassword = await bcrypt.hash(userData.password, 10); + + // Create user + const user = await this.userRepository.create({ + ...userData, + password: hashedPassword, + }); + + // Remove password from response + const { password, ...userWithoutPassword } = user; + return userWithoutPassword as User; + } + + async getUserById(id: string): Promise { + const user = await this.userRepository.findById(id); + if (!user) { + throw new NotFoundError("User not found"); + } + const { password, ...userWithoutPassword } = user; + return userWithoutPassword as User; + } + + async updateUser(id: string, updates: UpdateUserDTO): Promise { + const user = await this.userRepository.update(id, updates); + if (!user) { + throw new NotFoundError("User not found"); + } + const { password, ...userWithoutPassword } = user; + return userWithoutPassword as User; + } + + async deleteUser(id: string): Promise { + const deleted = await this.userRepository.delete(id); + if (!deleted) { + throw new NotFoundError("User not found"); + } + } +} +``` + +**Repository Layer:** + +```typescript +// repositories/user.repository.ts +import { Pool } from "pg"; +import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types"; + +export class UserRepository { + constructor(private db: Pool) {} + + async create( + userData: CreateUserDTO & { password: string }, + ): Promise { + const query = ` + INSERT INTO users (name, email, password) + VALUES ($1, $2, $3) + RETURNING id, name, email, password, created_at, updated_at + `; + const { rows } = await this.db.query(query, [ + userData.name, + userData.email, + userData.password, + ]); + return rows[0]; + } + + async findById(id: string): Promise { + const query = "SELECT * FROM users WHERE id = $1"; + const { rows } = await this.db.query(query, [id]); + return rows[0] || null; + } + + async findByEmail(email: string): Promise { + const query = "SELECT * FROM users WHERE email = $1"; + const { rows } = await this.db.query(query, [email]); + return rows[0] || null; + } + + async update(id: string, updates: UpdateUserDTO): Promise { + const fields = Object.keys(updates); + const values = Object.values(updates); + + const setClause = fields + .map((field, idx) => `${field} = $${idx + 2}`) + .join(", "); + + const query = ` + UPDATE users + SET ${setClause}, updated_at = CURRENT_TIMESTAMP + WHERE id = $1 + RETURNING * + `; + + const { rows } = await this.db.query(query, [id, ...values]); + return rows[0] || null; + } + + async delete(id: string): Promise { + const query = "DELETE FROM users WHERE id = $1"; + const { rowCount } = await this.db.query(query, [id]); + return rowCount > 0; + } +} +``` + +### Pattern 2: Dependency Injection + +**DI Container:** + +```typescript +// di-container.ts +import { Pool } from "pg"; +import { UserRepository } from "./repositories/user.repository"; +import { UserService } from "./services/user.service"; +import { UserController } from "./controllers/user.controller"; +import { AuthService } from "./services/auth.service"; + +class Container { + private instances = new Map(); + + register(key: string, factory: () => T): void { + this.instances.set(key, factory); + } + + resolve(key: string): T { + const factory = this.instances.get(key); + if (!factory) { + throw new Error(`No factory registered for ${key}`); + } + return factory(); + } + + singleton(key: string, factory: () => T): void { + let instance: T; + this.instances.set(key, () => { + if (!instance) { + instance = factory(); + } + return instance; + }); + } +} + +export const container = new Container(); + +// Register dependencies +container.singleton( + "db", + () => + new Pool({ + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || "5432"), + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, + }), +); + +container.singleton( + "userRepository", + () => new UserRepository(container.resolve("db")), +); + +container.singleton( + "userService", + () => new UserService(container.resolve("userRepository")), +); + +container.register( + "userController", + () => new UserController(container.resolve("userService")), +); + +container.singleton( + "authService", + () => new AuthService(container.resolve("userRepository")), +); +``` + +## Middleware Patterns + +### Authentication Middleware + +```typescript +// middleware/auth.middleware.ts +import { Request, Response, NextFunction } from "express"; +import jwt from "jsonwebtoken"; +import { UnauthorizedError } from "../utils/errors"; + +interface JWTPayload { + userId: string; + email: string; +} + +declare global { + namespace Express { + interface Request { + user?: JWTPayload; + } + } +} + +export const authenticate = async ( + req: Request, + res: Response, + next: NextFunction, +) => { + try { + const token = req.headers.authorization?.replace("Bearer ", ""); + + if (!token) { + throw new UnauthorizedError("No token provided"); + } + + const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload; + + req.user = payload; + next(); + } catch (error) { + next(new UnauthorizedError("Invalid token")); + } +}; + +export const authorize = (...roles: string[]) => { + return async (req: Request, res: Response, next: NextFunction) => { + if (!req.user) { + return next(new UnauthorizedError("Not authenticated")); + } + + // Check if user has required role + const hasRole = roles.some((role) => req.user?.roles?.includes(role)); + + if (!hasRole) { + return next(new UnauthorizedError("Insufficient permissions")); + } + + next(); + }; +}; +``` + +### Validation Middleware + +```typescript +// middleware/validation.middleware.ts +import { Request, Response, NextFunction } from "express"; +import { AnyZodObject, ZodError } from "zod"; +import { ValidationError } from "../utils/errors"; + +export const validate = (schema: AnyZodObject) => { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await schema.parseAsync({ + body: req.body, + query: req.query, + params: req.params, + }); + next(); + } catch (error) { + if (error instanceof ZodError) { + const errors = error.errors.map((err) => ({ + field: err.path.join("."), + message: err.message, + })); + next(new ValidationError("Validation failed", errors)); + } else { + next(error); + } + } + }; +}; + +// Usage with Zod +import { z } from "zod"; + +const createUserSchema = z.object({ + body: z.object({ + name: z.string().min(1), + email: z.string().email(), + password: z.string().min(8), + }), +}); + +router.post("/users", validate(createUserSchema), userController.createUser); +``` + +### Rate Limiting Middleware + +```typescript +// middleware/rate-limit.middleware.ts +import rateLimit from "express-rate-limit"; +import RedisStore from "rate-limit-redis"; +import Redis from "ioredis"; + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT || "6379"), +}); + +export const apiLimiter = rateLimit({ + store: new RedisStore({ + client: redis, + prefix: "rl:", + }), + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per windowMs + message: "Too many requests from this IP, please try again later", + standardHeaders: true, + legacyHeaders: false, +}); + +export const authLimiter = rateLimit({ + store: new RedisStore({ + client: redis, + prefix: "rl:auth:", + }), + windowMs: 15 * 60 * 1000, + max: 5, // Stricter limit for auth endpoints + skipSuccessfulRequests: true, +}); +``` + +### Request Logging Middleware + +```typescript +// middleware/logger.middleware.ts +import { Request, Response, NextFunction } from "express"; +import pino from "pino"; + +const logger = pino({ + level: process.env.LOG_LEVEL || "info", + transport: { + target: "pino-pretty", + options: { colorize: true }, + }, +}); + +export const requestLogger = ( + req: Request, + res: Response, + next: NextFunction, +) => { + const start = Date.now(); + + // Log response when finished + res.on("finish", () => { + const duration = Date.now() - start; + logger.info({ + method: req.method, + url: req.url, + status: res.statusCode, + duration: `${duration}ms`, + userAgent: req.headers["user-agent"], + ip: req.ip, + }); + }); + + next(); +}; + +export { logger }; +``` + +## Error Handling + +### Custom Error Classes + +```typescript +// utils/errors.ts +export class AppError extends Error { + constructor( + public message: string, + public statusCode: number = 500, + public isOperational: boolean = true, + ) { + super(message); + Object.setPrototypeOf(this, AppError.prototype); + Error.captureStackTrace(this, this.constructor); + } +} + +export class ValidationError extends AppError { + constructor( + message: string, + public errors?: any[], + ) { + super(message, 400); + } +} + +export class NotFoundError extends AppError { + constructor(message: string = "Resource not found") { + super(message, 404); + } +} + +export class UnauthorizedError extends AppError { + constructor(message: string = "Unauthorized") { + super(message, 401); + } +} + +export class ForbiddenError extends AppError { + constructor(message: string = "Forbidden") { + super(message, 403); + } +} + +export class ConflictError extends AppError { + constructor(message: string) { + super(message, 409); + } +} +``` + +### Global Error Handler + +```typescript +// middleware/error-handler.ts +import { Request, Response, NextFunction } from "express"; +import { AppError } from "../utils/errors"; +import { logger } from "./logger.middleware"; + +export const errorHandler = ( + err: Error, + req: Request, + res: Response, + next: NextFunction, +) => { + if (err instanceof AppError) { + return res.status(err.statusCode).json({ + status: "error", + message: err.message, + ...(err instanceof ValidationError && { errors: err.errors }), + }); + } + + // Log unexpected errors + logger.error({ + error: err.message, + stack: err.stack, + url: req.url, + method: req.method, + }); + + // Don't leak error details in production + const message = + process.env.NODE_ENV === "production" + ? "Internal server error" + : err.message; + + res.status(500).json({ + status: "error", + message, + }); +}; + +// Async error wrapper +export const asyncHandler = ( + fn: (req: Request, res: Response, next: NextFunction) => Promise, +) => { + return (req: Request, res: Response, next: NextFunction) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +}; +``` + +## Database Patterns + +### PostgreSQL with Connection Pool + +```typescript +// config/database.ts +import { Pool, PoolConfig } from "pg"; + +const poolConfig: PoolConfig = { + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT || "5432"), + database: process.env.DB_NAME, + user: process.env.DB_USER, + password: process.env.DB_PASSWORD, + max: 20, + idleTimeoutMillis: 30000, + connectionTimeoutMillis: 2000, +}; + +export const pool = new Pool(poolConfig); + +// Test connection +pool.on("connect", () => { + console.log("Database connected"); +}); + +pool.on("error", (err) => { + console.error("Unexpected database error", err); + process.exit(-1); +}); + +// Graceful shutdown +export const closeDatabase = async () => { + await pool.end(); + console.log("Database connection closed"); +}; +``` + +### MongoDB with Mongoose + +```typescript +// config/mongoose.ts +import mongoose from "mongoose"; + +const connectDB = async () => { + try { + await mongoose.connect(process.env.MONGODB_URI!, { + maxPoolSize: 10, + serverSelectionTimeoutMS: 5000, + socketTimeoutMS: 45000, + }); + + console.log("MongoDB connected"); + } catch (error) { + console.error("MongoDB connection error:", error); + process.exit(1); + } +}; + +mongoose.connection.on("disconnected", () => { + console.log("MongoDB disconnected"); +}); + +mongoose.connection.on("error", (err) => { + console.error("MongoDB error:", err); +}); + +export { connectDB }; + +// Model example +import { Schema, model, Document } from "mongoose"; + +interface IUser extends Document { + name: string; + email: string; + password: string; + createdAt: Date; + updatedAt: Date; +} + +const userSchema = new Schema( + { + name: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + }, + { + timestamps: true, + }, +); + +// Indexes +userSchema.index({ email: 1 }); + +export const User = model("User", userSchema); +``` + +### Transaction Pattern + +```typescript +// services/order.service.ts +import { Pool } from "pg"; + +export class OrderService { + constructor(private db: Pool) {} + + async createOrder(userId: string, items: any[]) { + const client = await this.db.connect(); + + try { + await client.query("BEGIN"); + + // Create order + const orderResult = await client.query( + "INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id", + [userId, calculateTotal(items)], + ); + const orderId = orderResult.rows[0].id; + + // Create order items + for (const item of items) { + await client.query( + "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)", + [orderId, item.productId, item.quantity, item.price], + ); + + // Update inventory + await client.query( + "UPDATE products SET stock = stock - $1 WHERE id = $2", + [item.quantity, item.productId], + ); + } + + await client.query("COMMIT"); + return orderId; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } + } +} +``` + +## Authentication & Authorization + +### JWT Authentication + +```typescript +// services/auth.service.ts +import jwt from "jsonwebtoken"; +import bcrypt from "bcrypt"; +import { UserRepository } from "../repositories/user.repository"; +import { UnauthorizedError } from "../utils/errors"; + +export class AuthService { + constructor(private userRepository: UserRepository) {} + + async login(email: string, password: string) { + const user = await this.userRepository.findByEmail(email); + + if (!user) { + throw new UnauthorizedError("Invalid credentials"); + } + + const isValid = await bcrypt.compare(password, user.password); + + if (!isValid) { + throw new UnauthorizedError("Invalid credentials"); + } + + const token = this.generateToken({ + userId: user.id, + email: user.email, + }); + + const refreshToken = this.generateRefreshToken({ + userId: user.id, + }); + + return { + token, + refreshToken, + user: { + id: user.id, + name: user.name, + email: user.email, + }, + }; + } + + async refreshToken(refreshToken: string) { + try { + const payload = jwt.verify( + refreshToken, + process.env.REFRESH_TOKEN_SECRET!, + ) as { userId: string }; + + const user = await this.userRepository.findById(payload.userId); + + if (!user) { + throw new UnauthorizedError("User not found"); + } + + const token = this.generateToken({ + userId: user.id, + email: user.email, + }); + + return { token }; + } catch (error) { + throw new UnauthorizedError("Invalid refresh token"); + } + } + + private generateToken(payload: any): string { + return jwt.sign(payload, process.env.JWT_SECRET!, { + expiresIn: "15m", + }); + } + + private generateRefreshToken(payload: any): string { + return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, { + expiresIn: "7d", + }); + } +} +``` + +## Caching Strategies + +```typescript +// utils/cache.ts +import Redis from "ioredis"; + +const redis = new Redis({ + host: process.env.REDIS_HOST, + port: parseInt(process.env.REDIS_PORT || "6379"), + retryStrategy: (times) => { + const delay = Math.min(times * 50, 2000); + return delay; + }, +}); + +export class CacheService { + async get(key: string): Promise { + const data = await redis.get(key); + return data ? JSON.parse(data) : null; + } + + async set(key: string, value: any, ttl?: number): Promise { + const serialized = JSON.stringify(value); + if (ttl) { + await redis.setex(key, ttl, serialized); + } else { + await redis.set(key, serialized); + } + } + + async delete(key: string): Promise { + await redis.del(key); + } + + async invalidatePattern(pattern: string): Promise { + const keys = await redis.keys(pattern); + if (keys.length > 0) { + await redis.del(...keys); + } + } +} + +// Cache decorator +export function Cacheable(ttl: number = 300) { + return function ( + target: any, + propertyKey: string, + descriptor: PropertyDescriptor, + ) { + const originalMethod = descriptor.value; + + descriptor.value = async function (...args: any[]) { + const cache = new CacheService(); + const cacheKey = `${propertyKey}:${JSON.stringify(args)}`; + + const cached = await cache.get(cacheKey); + if (cached) { + return cached; + } + + const result = await originalMethod.apply(this, args); + await cache.set(cacheKey, result, ttl); + + return result; + }; + + return descriptor; + }; +} +``` + +## API Response Format + +```typescript +// utils/response.ts +import { Response } from "express"; + +export class ApiResponse { + static success( + res: Response, + data: T, + message?: string, + statusCode = 200, + ) { + return res.status(statusCode).json({ + status: "success", + message, + data, + }); + } + + static error(res: Response, message: string, statusCode = 500, errors?: any) { + return res.status(statusCode).json({ + status: "error", + message, + ...(errors && { errors }), + }); + } + + static paginated( + res: Response, + data: T[], + page: number, + limit: number, + total: number, + ) { + return res.json({ + status: "success", + data, + pagination: { + page, + limit, + total, + pages: Math.ceil(total / limit), + }, + }); + } +} +``` + +## Best Practices + +1. **Use TypeScript**: Type safety prevents runtime errors +2. **Implement proper error handling**: Use custom error classes +3. **Validate input**: Use libraries like Zod or Joi +4. **Use environment variables**: Never hardcode secrets +5. **Implement logging**: Use structured logging (Pino, Winston) +6. **Add rate limiting**: Prevent abuse +7. **Use HTTPS**: Always in production +8. **Implement CORS properly**: Don't use `*` in production +9. **Use dependency injection**: Easier testing and maintenance +10. **Write tests**: Unit, integration, and E2E tests +11. **Handle graceful shutdown**: Clean up resources +12. **Use connection pooling**: For databases +13. **Implement health checks**: For monitoring +14. **Use compression**: Reduce response size +15. **Monitor performance**: Use APM tools + +## Testing Patterns + +See `javascript-testing-patterns` skill for comprehensive testing guidance. + +## Resources + +- **Node.js Best Practices**: https://github.com/goldbergyoni/nodebestpractices +- **Express.js Guide**: https://expressjs.com/en/guide/ +- **Fastify Documentation**: https://www.fastify.io/docs/ +- **TypeScript Node Starter**: https://github.com/microsoft/TypeScript-Node-Starter diff --git a/Backend/.claude/skills/nodejs-backend-patterns b/Backend/.claude/skills/nodejs-backend-patterns new file mode 120000 index 00000000..1c65884f --- /dev/null +++ b/Backend/.claude/skills/nodejs-backend-patterns @@ -0,0 +1 @@ +../../.agents/skills/nodejs-backend-patterns \ No newline at end of file diff --git a/Backend/.codex/skills/nodejs-backend-patterns b/Backend/.codex/skills/nodejs-backend-patterns new file mode 120000 index 00000000..1c65884f --- /dev/null +++ b/Backend/.codex/skills/nodejs-backend-patterns @@ -0,0 +1 @@ +../../.agents/skills/nodejs-backend-patterns \ No newline at end of file diff --git a/Backend/.cursor/skills/nodejs-backend-patterns b/Backend/.cursor/skills/nodejs-backend-patterns new file mode 120000 index 00000000..1c65884f --- /dev/null +++ b/Backend/.cursor/skills/nodejs-backend-patterns @@ -0,0 +1 @@ +../../.agents/skills/nodejs-backend-patterns \ No newline at end of file diff --git a/Backend/.gemini/skills/nodejs-backend-patterns b/Backend/.gemini/skills/nodejs-backend-patterns new file mode 120000 index 00000000..1c65884f --- /dev/null +++ b/Backend/.gemini/skills/nodejs-backend-patterns @@ -0,0 +1 @@ +../../.agents/skills/nodejs-backend-patterns \ No newline at end of file diff --git a/Backend/.windsurf/skills/nodejs-backend-patterns b/Backend/.windsurf/skills/nodejs-backend-patterns new file mode 120000 index 00000000..1c65884f --- /dev/null +++ b/Backend/.windsurf/skills/nodejs-backend-patterns @@ -0,0 +1 @@ +../../.agents/skills/nodejs-backend-patterns \ No newline at end of file diff --git a/Backend/src/controllers/CoursesController.ts b/Backend/src/controllers/CoursesController.ts index e6f210d4..5db57018 100644 --- a/Backend/src/controllers/CoursesController.ts +++ b/Backend/src/controllers/CoursesController.ts @@ -24,6 +24,6 @@ export class CoursesController { @SuccessResponse('200', 'Course fetched successfully') @Response('401', 'Invalid or expired token') public async getCourseById(@Path() id: number): Promise { - return await this.coursesService.ListCourses(); + return await this.coursesService.ListCourses(id); } }