Welcome to the DevSphere Backend project! This guide will help you understand our conventions, patterns, and best practices for contributing to this TypeScript/Express API.
- Getting Started
- Project Structure
- Naming Conventions
- Service Layer Conventions
- Controller Layer Conventions
- Route Definitions
- Error Handling
- Database Operations
- Type Definitions
- Validation & Parsing
- Authentication & Authorization
- Code Style Guidelines
src/
├── controllers/ # Request handlers and business logic coordination
├── services/ # Business logic and database operations
├── routers/ # Route definitions and middleware
├── middleware/ # Custom middleware functions
├── lib/ # Utility libraries (auth, jwt, password, etc.)
├── utils/ # Utility functions and constants
├── types/ # TypeScript type definitions
├── dtos/ # Data Transfer Objects for responses
├── jobs/ # Background jobs and cron tasks
├── db/ # Database configuration
└── index.ts # Application entry point
prisma/
├── schema/ # Prisma schema files
└── migrations/ # Database migration files
docs/ # API documentation
- Use kebab-case for file names:
event.controller.ts,member.service.ts - Use camelCase for directories:
controllers/,services/
- Use PascalCase for class names:
EventController,MemberService - Export instances using camelCase:
export const eventController = new EventController()
- Use camelCase for function names:
createEvent,getUserRole - Use descriptive verbs for CRUD operations:
createfor POST operationsget/fetchfor GET operationsupdatefor PUT/PATCH operationsremove/deletefor DELETE operations
- Use camelCase for variables:
eventData,userId - Use SCREAMING_SNAKE_CASE for constants:
HTTP.INTERNAL,DATABASE_URL
Services contain the core business logic and database operations. They follow a consistent return pattern.
ALL services MUST return an object with the following structure:
// Success Response
{
success: true,
data?: T // Optional data payload
}
// Error Response
{
success: false,
error: string, // Error message
message?: string // Optional additional message
}import prisma from '@/db/prisma';
import { prismaSafe } from '@/lib/prismaSafe';
class ExampleService {
async createExample(data: ExampleType) {
try {
const [dbError, result] = await prismaSafe(
prisma.example.create({
data: {
...data,
},
})
);
if (dbError) {
return { success: false, error: dbError };
}
if (!result) {
return { success: false, error: 'Failed to create example' };
}
return { success: true, data: result };
} catch (error) {
console.log(`Failed to create example: ${error}`);
return { success: false, error: 'Failed to create example' };
}
}
}
export const exampleService = new ExampleService();create{Resource}- Create new resourceget{Resource}orfetch{Resource}- Retrieve resource(s)update{Resource}- Update existing resourceremove{Resource}ordelete{Resource}- Delete resource
- Public methods at the top
- Private helpers at the bottom (prefixed with
_)
Controllers handle HTTP requests, validate input, call services, and return responses.
import type { Request, Response } from 'express';
import { ErrorResponse, SuccessResponse } from '@/dtos';
import { HTTP } from '@/utils/constants';
import { exampleService } from '@/services/example.service';
import exampleParser from '@/parser/example/example.parser';
class ExampleController {
async createExample(req: Request, res: Response) {
try {
// 1. Parse and validate input
const parseResult = await exampleParser(req.body);
if (!parseResult.success) {
return res
.status(HTTP.BAD_REQUEST)
.json(ErrorResponse(HTTP.BAD_REQUEST, parseResult.error || 'Invalid data'));
}
// 2. Call service
const serviceResult = await exampleService.createExample(parseResult.data);
if (!serviceResult.success) {
return res
.status(HTTP.INTERNAL)
.json(ErrorResponse(HTTP.INTERNAL, serviceResult.error || 'Failed to create'));
}
// 3. Return success response
return res
.status(HTTP.CREATED)
.json(SuccessResponse(HTTP.CREATED, 'Created successfully', serviceResult.data));
} catch (error) {
return res.status(HTTP.INTERNAL).json(ErrorResponse(HTTP.INTERNAL, 'Internal Server Error'));
}
}
}
export const exampleController = new ExampleController();Always use the standardized response helpers:
// Success Response
res.status(HTTP.CREATED).json(
SuccessResponse(statusCode, message, data)
);
// Error Response
res.status(HTTP.BAD_REQUEST).json(
ErrorResponse(statusCode, errorMessage, details?)
);Routes should be organized by resource and follow RESTful conventions.
const exampleRouter = Router();
// Public routes
exampleRouter.get('/', exampleController.getExamples);
exampleRouter.get('/:id', exampleController.getExample);
// Authenticated routes
exampleRouter.use(authMiddleware); // Apply to all routes below
exampleRouter.post('/', exampleController.createExample);
exampleRouter.put('/:id', exampleController.updateExample);
// Admin only routes
exampleRouter.delete('/:id', isAdmin, exampleController.deleteExample);
export default exampleRouter;| HTTP Method | Route Pattern | Purpose | Controller Method |
|---|---|---|---|
GET |
/api/resource |
Get all resources | getResources |
GET |
/api/resource/:id |
Get single resource | getResource |
POST |
/api/resource |
Create new resource | createResource |
PUT |
/api/resource/:id |
Update entire resource | updateResource |
PATCH |
/api/resource/:id |
Partial update | patchResource |
DELETE |
/api/resource/:id |
Delete resource | deleteResource |
// In src/index.ts
app.use('/api/examples', exampleRouter);
app.use('/api/events', eventRouter);
app.use('/api/members', memberRouter);Use the constants from @/utils/constants:
Always use prismaSafe for database operations:
const [error, result] = await prismaSafe(prisma.model.operation());
if (error) {
return { success: false, error };
}- Always use
prismaSafewrapper for database operations - Check for errors first, then check for null results
- Use meaningful error messages
const [error, user] = await prismaSafe(
prisma.user.findUnique({
where: { id: userId },
})
);
if (error) {
return { success: false, error };
}
if (!user) {
return { success: false, error: 'User not found' };
}After any Prisma schema changes:
- Run
npx prisma migrate devto update the database - Run
npx prisma generateto update the Prisma client
- Place shared types in
src/types/ - Use descriptive names:
userTypes.ts,member.types.ts - Export interfaces and types clearly
// src/types/example.types.ts
export interface Example {
id: string;
name: string;
createdAt: Date;
updatedAt: Date;
}
export interface CreateExampleRequest {
name: string;
description?: string;
}Place validation schemas in src/utils/types/:
import { z } from 'zod';
export const exampleSchema = z
.object({
name: z.string().min(1, 'Name is required'),
description: z.string().optional(),
})
.strict();
export type Example = z.infer<typeof exampleSchema>;use Zod validation middleware
Apply authentication middleware appropriately:
// For all routes in a router
router.use(authMiddleware);
// For specific routes
router.post('/admin-only', isAdmin, controller.method);
// For role-based access
router.get('/moderator', isModerator, controller.method);In controllers, access user ID from request:
async createExample(req: Request, res: Response) {
const userId = req.userId; // Available after authMiddleware
// ... rest of controller logic
}Use consistent error logging:
console.log(`Failed to ${operation}: ${error}`);- External libraries first
- Internal utilities and types
- Local imports (controllers, services)
import { z } from 'zod';
import type { Request, Response } from 'express';
import { HTTP } from '@/utils/constants';
import type { Example } from '@/types/example.types';
import { exampleService } from '@/services/example.service';- Add all environment variables to
.env.example - Use TypeScript for environment validation
- Access via
process.env.VARIABLE_NAME
- Write descriptive commit messages
- Test your changes thoroughly
- Update documentation if needed
- Follow all conventions outlined in this guide
type: description
Examples:
feat: add user profile endpoint
fix: resolve authentication middleware issue
docs: update API documentation
refactor: improve error handling in user service
If you have questions about these conventions or need clarification on any patterns, please:
- Check existing code for examples
- Create an issue for discussion
- Ask in team communication channels
Thank you for contributing to DevSphere Backend!