Skip to content

Backend Guidelines

  • NestJS
  • TypeScript
  • MikroORM
  • Zod
  • Swagger/OpenAPI
  • Better Auth

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
├── feature-name.mapper.ts # Mapper for the entity (transform entity to contract, used in the controller)
└── 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 together
└── tests/
└── feature-name.controller.e2e-spec.ts
  • 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)

There is no module generator. Copy apps/api/src/modules/example/posts/ and adapt it. That folder is the reference for entity, contract, mapper, service, controller, module, and e2e test.

Then follow these steps (adapt as needed):

  1. Create an entity, using posts.entity.ts (or another existing entity file if not found) as reference;
  2. 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);
  3. Create a mapper, using posts.mapper.ts (or another existing mapper file if not found) as reference;
  4. Create a service, using posts.service.ts (or another existing service file if not found) as reference;
  5. Create a controller, using posts.controller.ts (or another existing controller file if not found) as reference;
  6. Create a module, using posts.module.ts (or another existing module file if not found) as reference;
  7. Add an e2e test under tests/, using posts.controller.e2e-spec.ts (or another existing e2e test if not found) as reference. See API testing for how those tests run.
  • Use decorators from NestJS for route definition
  • Group related endpoints under the same controller
  • Use versioning when making breaking changes
  • Use proper HTTP methods:
    • GET for retrieving data
    • POST for creating resources
    • PUT for full updates
    • PATCH for partial updates
    • DELETE for removing resources
  • 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

Why use .meta()?

  • .meta() adds OpenAPI/Swagger documentation metadata to your Zod schemas
  • This metadata is automatically used by @lonestone/nzoth/server to generate API documentation
  • It provides better developer experience with auto-generated OpenAPI specs
  • The metadata includes title, description, examples, and other OpenAPI-specific information
  • Enums should be present in the contract files, not in the entities. And they should be exposed via meta() to be used on the frontends.

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 rules
export 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 usage
export type CreateUserInput = z.infer<typeof createUserSchema>
// Controller usage
@TypedController('user')
export class UserController {
@TypedRoute.Post('', createUserSchema)
async createUser(@TypedBody(createUserSchema) body: CreateUserInput) {
// Implementation
}
}

Create vs Update Schemas:

// Base schema for common fields
const 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',
})
  • Use consistent response formats
  • Return typed responses using Zod schemas
  • Document responses with OpenAPI annotations
  • Services should usually return entities, to be used accross other services without the need to transform contract back to entities (or re-fetch from the database). If the project aims for a full hexagonal architecture, you can return contracts from the services but they should be different from the contracts used in the controller.
  • It’s a good practise to use a mapper to transform the entity to the contract in the controller.
  • Mappers can be static class or injectable service when they need to use other services.
  • Mappers should be named entity.mapper.ts
  • Mappers should handle collections gracefully, using MikroORM getItems after checking if the collection has been initialized via entity.collection.isInitialized()
  • Use NestJS exceptions for error handling
  • Return appropriate HTTP status codes
  • Provide meaningful error messages
  • Use exception filters for global error handling
  • 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()
}
  • 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
  • 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
}
  • 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
  • Write unit tests for services
  • Write integration tests for controllers
  • Use vitest for testing
  • Mock external dependencies