Backend FAQ
Here are answers to frequently asked questions about TrioSigno's backend.
Architecture and Technologies
What technologies are used for TrioSigno's backend?
TrioSigno's backend is built with the following technologies:
- Node.js as the runtime environment
- Express as the web framework
- TypeScript for static typing
- Prisma as the ORM (Object-Relational Mapping)
- PostgreSQL as the main database
- Redis for caching and sessions
- JWT (JSON Web Tokens) for authentication
- Socket.io for real-time communications
- Jest for unit and integration testing
- Supertest for API testing
- Docker for containerization
How is the backend architecture organized?
The backend architecture follows a layered approach:
- Routes: Define API endpoints and direct requests to controllers
- Controllers: Handle HTTP request processing logic
- Services: Contain the main business logic
- Repositories: Manage database interactions
- Models: Define data structure (via Prisma)
- Middleware: Intermediate functions for authentication, validation, etc.
- Utils: Reusable utility functions
This architecture allows for a clear separation of concerns and facilitates maintenance and testing.
How is the database managed?
The database is managed through several mechanisms:
- Prisma Schema: The
schema.prisma
file defines all data models and their relationships - Migrations: Schema changes are managed via Prisma migrations for safe deployment
- Seed: Initial data can be loaded via seed scripts
- Transactions: Complex operations are encapsulated in transactions to ensure data integrity
- Optimization: Indexes are defined for frequent queries, and relations are loaded with
include
as needed
Example of a Prisma schema for a User
model and its relationships:
model User {
id String @id @default(uuid())
email String @unique
username String @unique
password String
firstName String?
lastName String?
role UserRole @default(STUDENT)
profilePicture String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isActive Boolean @default(true)
progress Progress[]
sessions LearningSession[]
achievements UserAchievement[]
userPreferences UserPreference?
}
enum UserRole {
ADMIN
TEACHER
STUDENT
}
Development
How do I set up the backend development environment?
To set up the backend development environment:
-
Clone the Git repository:
git clone https://github.com/triosigno/triosigno.git
cd triosigno/server -
Install dependencies:
npm install
-
Configure environment variables:
- Copy the
.env.example
file to.env
- Modify the values according to your configuration
- Copy the
-
Set up the database:
# Start PostgreSQL (if not already running)
docker-compose up -d db
# Generate Prisma client
npx prisma generate
# Run migrations
npx prisma migrate dev
# (Optional) Seed the database
npm run seed -
Start the development server:
npm run dev
The server will be available at http://localhost:4000 by default.
How do I create a new API endpoint?
To create a new API endpoint:
-
Define the route in the appropriate router file (e.g.,
src/routes/lessonRoutes.ts
):import express from "express";
import { lessonController } from "../controllers/lessonController";
import { authenticate } from "../middleware/auth";
const router = express.Router();
router.get("/", lessonController.getAllLessons);
router.get("/:id", lessonController.getLessonById);
router.post("/", authenticate, lessonController.createLesson);
router.put("/:id", authenticate, lessonController.updateLesson);
router.delete("/:id", authenticate, lessonController.deleteLesson);
export default router; -
Create or update the controller (e.g.,
src/controllers/lessonController.ts
):import { Request, Response } from "express";
import { lessonService } from "../services/lessonService";
export const lessonController = {
async getAllLessons(req: Request, res: Response) {
try {
const lessons = await lessonService.getAllLessons();
return res.status(200).json(lessons);
} catch (error) {
return res
.status(500)
.json({ message: "Error fetching lessons", error });
}
},
async getLessonById(req: Request, res: Response) {
try {
const { id } = req.params;
const lesson = await lessonService.getLessonById(id);
if (!lesson) {
return res.status(404).json({ message: "Lesson not found" });
}
return res.status(200).json(lesson);
} catch (error) {
return res
.status(500)
.json({ message: "Error fetching lesson", error });
}
},
// Additional controller methods...
}; -
Implement the service (e.g.,
src/services/lessonService.ts
):import { prisma } from "../lib/prisma";
import { Lesson, Prisma } from "@prisma/client";
export const lessonService = {
async getAllLessons() {
return prisma.lesson.findMany({
include: {
category: true,
level: true,
},
});
},
async getLessonById(id: string) {
return prisma.lesson.findUnique({
where: { id },
include: {
category: true,
level: true,
exercises: true,
},
});
},
// Additional service methods...
}; -
Register the router in the main app (if it's a new route group):
import lessonRoutes from "./routes/lessonRoutes";
// ...
app.use("/api/lessons", lessonRoutes);
How are environment variables handled?
Environment variables in TrioSigno's backend are handled using the dotenv
package:
-
Create a
.env
file in the root directory (based on.env.example
) -
Load environment variables at the beginning of the application:
// src/config/index.ts
import dotenv from "dotenv";
// Load environment variables
dotenv.config();
export default {
port: process.env.PORT || 4000,
nodeEnv: process.env.NODE_ENV || "development",
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
jwtExpiresIn: process.env.JWT_EXPIRES_IN || "1d",
redisUrl: process.env.REDIS_URL,
corsOrigin: process.env.CORS_ORIGIN || "http://localhost:3000",
// Other configuration variables...
}; -
Access configuration values from this central config:
import config from "./config";
const server = app.listen(config.port, () => {
console.log(`Server running on port ${config.port}`);
});
Different environment variables are used for different environments (development, testing, production) to ensure proper configuration.
Testing and Quality
How do I run backend tests?
To run backend tests:
-
Unit and integration tests with Jest:
# Run all tests
npm test
# Run tests in watch mode (for development)
npm run test:watch
# Run tests with coverage report
npm run test:coverage -
API tests with Supertest:
npm run test:api
-
E2E tests with Cypress (if applicable):
npm run test:e2e
Make sure the test database is configured properly in your .env.test
file.
How do I debug the backend?
To debug TrioSigno's backend:
-
Using VSCode debugger:
- Create a
.vscode/launch.json
file with the appropriate configuration - Set breakpoints in your code
- Use the "Run and Debug" panel in VSCode
- Create a
-
Using console:
- Add
console.log()
,console.debug()
, orconsole.error()
statements - Use the
debug
package for more structured logs
- Add
-
Using inspection:
- Start the application with the
--inspect
flag:node --inspect dist/index.js
- Connect to the debugger using Chrome DevTools or Node.js debugging tools
- Start the application with the
-
Testing specific parts:
- Create small test scripts to isolate and test specific functionality
- Use Jest's
.only
modifier to run only specific tests
How is error handling implemented?
Error handling in TrioSigno's backend is implemented at multiple levels:
-
Global error handler middleware:
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import { logger } from "../utils/logger";
export function errorHandler(
err: any,
req: Request,
res: Response,
next: NextFunction
) {
const status = err.statusCode || 500;
const message = err.message || "Internal Server Error";
logger.error(
`[${req.method}] ${req.path} >> StatusCode: ${status}, Message: ${message}`
);
// Handle different types of errors
if (err.name === "ValidationError") {
return res.status(400).json({ message, errors: err.errors });
}
if (err.name === "UnauthorizedError") {
return res.status(401).json({ message: "Unauthorized" });
}
// For security, don't expose internal error details in production
if (process.env.NODE_ENV === "production") {
return res.status(status).json({ message });
}
// In development, include the stack trace
return res.status(status).json({
message,
stack: err.stack,
});
} -
Custom error classes:
// src/utils/errors.ts
export class AppError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource = "Resource") {
super(`${resource} not found`, 404);
}
}
export class ValidationError extends AppError {
errors: any;
constructor(message: string, errors?: any) {
super(message, 400);
this.errors = errors;
}
}
// Other custom error classes... -
Try-catch blocks in controllers:
try {
const result = await someService.doSomething();
return res.status(200).json(result);
} catch (error) {
next(error); // Pass to error handler middleware
} -
Error logging using a structured logger like Winston or Pino
Authentication and Security
How is user authentication implemented?
User authentication in TrioSigno is implemented using JWT (JSON Web Tokens):
-
Registration:
- User submits registration data
- Password is hashed using bcrypt
- User record is created in the database
- A verification email is sent (optional)
-
Login:
- User submits credentials
- System verifies email/username and password
- If valid, a JWT token is generated and returned
- Token contains user ID and role information
-
Authentication middleware:
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express";
import jwt from "jsonwebtoken";
import config from "../config";
export function authenticate(
req: Request,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return res.status(401).json({ message: "Authentication required" });
}
const token = authHeader.split(" ")[1];
try {
const decoded = jwt.verify(token, config.jwtSecret);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ message: "Invalid or expired token" });
}
}
export function authorize(roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ message: "Authentication required" });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: "Insufficient permissions" });
}
next();
};
} -
Token refresh:
- A refresh token mechanism allows obtaining a new access token
- Refresh tokens have a longer lifespan but are stored securely
-
Logout:
- On logout, client removes the token
- For added security, tokens can be blacklisted using Redis
How is data security ensured?
Data security in TrioSigno is ensured through multiple measures:
- HTTPS: All communications are encrypted using TLS/SSL
- Password handling:
- Passwords are hashed using bcrypt with appropriate salt rounds
- Original passwords are never stored
- Input validation:
- All user inputs are validated using libraries like Joi or Zod
- Data is sanitized to prevent injection attacks
- Rate limiting:
- API endpoints are protected against brute-force attacks using rate limiting
- CORS configuration:
- Cross-Origin Resource Sharing is configured to allow only trusted origins
- Headers security:
- Security headers (Content-Security-Policy, X-XSS-Protection, etc.) are set
- Database security:
- Database access is restricted by network rules
- Queries use parameterized statements to prevent SQL injection
- Auditing:
- Security-relevant actions are logged for audit purposes
Performance and Scaling
How is the API performance optimized?
API performance in TrioSigno is optimized through several techniques:
-
Caching:
-
Redis is used to cache frequent queries and responses
-
Cache invalidation strategies ensure data consistency
-
Example implementation:
// src/services/lessonService.ts
import { redisClient } from "../lib/redis";
async function getLessonById(id: string) {
// Try to get from cache first
const cachedLesson = await redisClient.get(`lesson:${id}`);
if (cachedLesson) {
return JSON.parse(cachedLesson);
}
// If not in cache, get from database
const lesson = await prisma.lesson.findUnique({
where: { id },
include: {
/* ... */
},
});
// Store in cache for future requests
if (lesson) {
await redisClient.set(
`lesson:${id}`,
JSON.stringify(lesson),
"EX",
3600 // 1 hour expiration
);
}
return lesson;
}
-
-
Database optimization:
- Proper indexes on frequently queried fields
- Efficient query patterns using Prisma
- Database connection pooling
-
Pagination:
-
All list endpoints support pagination
-
Example implementation:
async function getLessons(page = 1, limit = 20) {
const skip = (page - 1) * limit;
const [lessons, total] = await Promise.all([
prisma.lesson.findMany({
skip,
take: limit,
include: {
/* ... */
},
}),
prisma.lesson.count(),
]);
return {
data: lessons,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}
-
-
Compression:
-
HTTP response compression using gzip or brotli
-
Implementation with Express middleware:
import compression from "compression";
app.use(compression());
-
-
Asynchronous processing:
- Long-running tasks are offloaded to background workers
- A job queue system using Bull/Redis handles processing
How is the backend structured for scaling?
TrioSigno's backend is structured for scaling in the following ways:
-
Stateless design:
- The API is stateless, allowing horizontal scaling
- Session data is stored in Redis, not in memory
-
Microservices approach (for larger deployments):
- Core services are separated into smaller, focused services
- Communication between services uses message queues or REST/gRPC
-
Load balancing:
- Multiple instances of the API can run behind a load balancer
- Health check endpoints ensure traffic is directed to healthy instances
-
Database scaling:
- Read replicas for read-heavy operations
- Connection pooling to optimize database connections
- Potential sharding for very large datasets
-
Caching strategy:
- Multi-level caching (application, database, CDN)
- Distributed cache using Redis Cluster
-
Container orchestration:
- Deployment using Kubernetes or similar for managing containers
- Auto-scaling based on traffic and resource utilization
-
Monitoring and observability:
- Prometheus for metrics collection
- Grafana for visualization
- Distributed tracing with OpenTelemetry
How are long-running tasks handled?
Long-running tasks in TrioSigno are handled using a job queue system:
-
Job queue implementation:
// src/lib/queue.ts
import Queue from "bull";
import config from "../config";
export const emailQueue = new Queue("email", config.redisUrl);
export const processingQueue = new Queue("processing", config.redisUrl);
export const aiModelQueue = new Queue("ai-model", config.redisUrl);
// Set up queue event handlers
emailQueue.on("completed", (job) => {
console.log(`Email job ${job.id} completed`);
});
emailQueue.on("failed", (job, err) => {
console.error(`Email job ${job.id} failed: ${err.message}`);
});
// Similar handlers for other queues -
Adding jobs to the queue:
// src/services/userService.ts
import { emailQueue } from "../lib/queue";
async function sendWelcomeEmail(user) {
// Add email job to queue instead of sending directly
await emailQueue.add(
"welcome-email",
{
to: user.email,
name: user.firstName,
userId: user.id,
},
{
attempts: 3,
backoff: {
type: "exponential",
delay: 5000,
},
}
);
} -
Processing jobs:
// src/workers/emailWorker.ts
import { emailQueue } from "../lib/queue";
import { emailService } from "../services/emailService";
emailQueue.process("welcome-email", async (job) => {
const { to, name, userId } = job.data;
// Send the actual email
await emailService.sendWelcomeEmail(to, name);
// Return job result
return { sent: true, to, timestamp: new Date() };
}); -
Worker management:
- Workers run in separate processes
- Concurrency is configured based on task type
- Retries and backoff strategies handle failures
-
Monitoring job queues:
- Queue dashboards provide visibility into job status
- Alerts notify of queue backlogs or high failure rates