Backend Guidelines
API Guidelines
Section titled “API Guidelines”- NestJS
- TypeScript
- MikroORM
- Zod
- Swagger/OpenAPI
- Better Auth
Architecture
Section titled “Architecture”Module Structure
Section titled “Module Structure”Each feature should be organized as a NestJS module with the following structure:
modules/feature-name/├── feature-name.module.ts # Module definition├── feature-name.controller.ts # HTTP endpoints├── feature-name.service.ts # Business logic├── feature-name.entity.ts # If there is a single entity, a single entity file is fine├── feature-name.contract.ts # If there is a single entity, a single contract is fine└── contracts/ # DTOs and validation schemas └── feature-name.contract.ts # If multiple contracts are needed, create a subfolder to store them together└── entities/ # Database entities └── feature-name.entity.ts # If multiple entities are needed, create a subfolder to store them togetherNaming Conventions
Section titled “Naming Conventions”- Files: Use kebab-case for filenames (e.g.,
user-profile.service.ts) - Classes: Use PascalCase for class names (e.g.,
UserProfileService) - Methods: Use camelCase for method names (e.g.,
getUserProfile) - Variables: Use camelCase for variable names (e.g.,
userProfile) - Constants: Use UPPER_SNAKE_CASE for constants (e.g.,
MAX_USERS) - Interfaces/Types: Use PascalCase prefixed with ‘I’ for interfaces (e.g.,
IUserProfile) - Enums: Use PascalCase for enum names (e.g.,
UserRole)
API Design
Section titled “API Design”To create a new module, you can use the following command:
pnpm generate:module --name=module-nameIt’s generated with the following files:
__name__.controller.ts__name__.service.ts__name__.entity.ts__name__.module.tscontracts/__name__.contract.tstests/__name__.controller.spec.ts
If you want to create a new module from scratch without the generator, you should follow the following steps (posts module is provided as an example because it’s present in the boilerplate, adapt as needed)
- Create an entity, using posts.entity.ts (or another existing entity file if not found) as reference;
- Create a contract, using posts.contract.ts (or another existing contract file if not found) as reference. a. The contract file should contain the schema for all CRUD actions (GET/POST/PUT/DELETE); b. If related contracts are needed (relations), create the related contract files first; c. When creating a contract, always start from the GET contract (nameOfEntitySchema) and then create the create and update contracts by extending the initial schema (you can use zod pick);
- Create a service, using posts.service.ts (or another existing service file if not found) as reference;
- Create a controller, using posts.controller.ts (or another existing controller file if not found) as reference;
- Create a module, using posts.module.ts (or another existing module file if not found) as reference;
Controllers
Section titled “Controllers”- Use decorators from NestJS for route definition
- Group related endpoints under the same controller
- Use versioning when making breaking changes
- Use proper HTTP methods:
GETfor retrieving dataPOSTfor creating resourcesPUTfor full updatesPATCHfor partial updatesDELETEfor removing resources
Request Validation
Section titled “Request Validation”- Use Zod schemas for request validation
- Define schemas in the contracts directory
- Use
TypedBody,TypedParam, and other typed decorators from@lonestone/nzoth/server - Export types from schemas using
z.infer<typeof schema> - Use
.meta()to add OpenAPI documentation metadata to schemas
Schema Definition Best Practices
Section titled “Schema Definition Best Practices”Why use .meta()?
.meta()adds OpenAPI/Swagger documentation metadata to your Zod schemas- This metadata is automatically used by
@lonestone/nzoth/serverto generate API documentation - It provides better developer experience with auto-generated OpenAPI specs
- The metadata includes title, description, examples, and other OpenAPI-specific information
Schema Structure:
- Start with basic validation rules (type, length, format, etc.)
- Add
.meta()for documentation purposes - Export the inferred type for TypeScript usage
- Use descriptive names that indicate the purpose (e.g.,
createUserSchema,updateUserSchema)
Example:
import { z } from 'zod'
// Basic schema with validation rulesexport const createUserSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Invalid email format'), age: z.number().min(18, 'User must be at least 18 years old').optional(),}).meta({ title: 'Create User', description: 'Schema for creating a new user account', examples: [ { name: 'John Doe', email: 'john.doe@example.com', age: 25 } ]})
// Export the inferred type for TypeScript usageexport type CreateUserInput = z.infer<typeof createUserSchema>
// Controller usage@TypedController('user')export class UserController { @TypedRoute.Post('', createUserSchema) async createUser(@TypedBody(createUserSchema) body: CreateUserInput) { // Implementation }}Common Schema Patterns
Section titled “Common Schema Patterns”Create vs Update Schemas:
// Base schema for common fieldsconst userBaseSchema = z.object({ name: z.string().min(2), email: z.string().email(),})
// Create schema (all fields required)export const createUserSchema = userBaseSchema.meta({ title: 'Create User', description: 'Create a new user account'})
// Update schema (all fields optional)export const updateUserSchema = userBaseSchema.partial().meta({ title: 'Update User', description: 'Update an existing user account'})Response Schemas:
export const userResponseSchema = z.object({ id: z.string().uuid(), name: z.string(), email: z.string().email(), createdAt: z.date(), updatedAt: z.date(),}).meta({ title: 'User Response', description: 'User data returned by the API'})Response Formatting
Section titled “Response Formatting”- Use consistent response formats
- Return typed responses using Zod schemas
- Document responses with OpenAPI annotations
Error Handling
Section titled “Error Handling”- Use NestJS exceptions for error handling
- Return appropriate HTTP status codes
- Provide meaningful error messages
- Use exception filters for global error handling
Database
Section titled “Database”Entities
Section titled “Entities”- Use MikroORM decorators for entity definition
- Follow single responsibility principle
- Use UUIDs for primary keys
- Include audit fields (createdAt, updatedAt)
- Define proper indexes for performance
- Use appropriate relationships (OneToMany, ManyToOne, etc.)
Example:
@Entity({ tableName: 'user' })export class User { @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' }) id!: string
@Property() @Index() name!: string
@Property({ fieldName: 'createdAt' }) createdAt: Date = new Date()
@Property({ fieldName: 'updatedAt', onUpdate: () => new Date() }) updatedAt: Date = new Date()}Queries
Section titled “Queries”- Use the EntityManager for database operations
- Use transactions for operations that modify multiple entities. MikroORM wraps each operation in a transaction by default, if they come from a controller.
- Ensure you have a MikroORM Context. Mikro provides one for each request by default, but if you call a method from a CRON you’ll need to create one or use the EnsureRequestContext decorator.
- Optimize queries for performance
- Use pagination for large result sets
Authentication & Authorization
Section titled “Authentication & Authorization”- Use Better Auth for authentication
- Use guards for protecting routes
- Use decorators for role-based access control
- Validate user permissions in services
Example:
@Controller('admin/users')@UseGuards(AuthGuard)export class UserController { // Protected endpoints}Documentation
Section titled “Documentation”- Use Swagger/OpenAPI for API documentation
- Document all endpoints, parameters, and responses
- Use tags to group related endpoints
- Provide examples for request and response bodies
Testing
Section titled “Testing”- Write unit tests for services
- Write integration tests for controllers
- Use Jest for testing
- Mock external dependencies