Overview
Postiz follows strict code standards to maintain consistency, readability, and quality across the codebase. These standards are enforced through ESLint, Prettier, and TypeScript.
General Principles
Language Requirements
TypeScript is required for all code. No JavaScript files except configuration.
// ✓ Correct - TypeScript with types
interface User {
id : string ;
email : string ;
name : string ;
}
function getUser ( id : string ) : Promise < User > {
return fetch ( `/api/users/ ${ id } ` );
}
// ✗ Wrong - No 'any' types
function getUser ( id : any ) : any {
return fetch ( `/api/users/ ${ id } ` );
}
Package Manager
Only use pnpm . Never use npm or yarn.
# ✓ Correct
pnpm install package-name
pnpm dev
pnpm build
# ✗ Wrong
npm install package-name
yarn add package-name
Monorepo Structure
All dependencies in root package.json
Shared code in libraries/
Apps in apps/
No app-specific node_modules
Backend Standards
Architecture Layers
Strict 3-layer pattern : Controller → Service → RepositoryNever skip layers. Sometimes 4 layers: Controller → Manager → Service → Repository
// ✓ Correct - Following layers
@ Controller ( '/posts' )
export class PostsController {
constructor ( private _postsService : PostsService ) {}
@ Get ()
async getPosts () {
return this . _postsService . list ();
}
}
@ Injectable ()
export class PostsService {
constructor ( private _postsRepository : PostsRepository ) {}
async list () {
return this . _postsRepository . findMany ();
}
}
// ✗ Wrong - Controller accessing repository directly
@ Controller ( '/posts' )
export class PostsController {
constructor ( private _postsRepository : PostsRepository ) {} // Wrong!
@ Get ()
async getPosts () {
return this . _postsRepository . findMany (); // Skipping service layer!
}
}
Service Location
Most server logic should be in libraries/nestjs-libraries/src/services, not in apps/backend.
✓ libraries/nestjs-libraries/src/services/email.service.ts
✓ libraries/nestjs-libraries/src/services/stripe.service.ts
✗ apps/backend/src/services/email.service.ts # Wrong location
Dependency Injection
// ✓ Correct - Constructor injection with private keyword
@ Injectable ()
export class PostsService {
constructor (
private _postsRepository : PostsRepository ,
private _usersService : UsersService ,
) {}
}
// ✗ Wrong - Manual instantiation
@ Injectable ()
export class PostsService {
private postsRepository = new PostsRepository (); // Wrong!
}
Naming Conventions
// Services - private with underscore prefix
private _userService : UserService
private _db : DatabaseService
// Methods - camelCase
async getUserById ( id : string ) {}
async createPost ( data : CreatePostDto ) {}
// Classes - PascalCase
export class PostsService {}
export class AuthController {}
// Interfaces - PascalCase with descriptive names
interface CreatePostDto {}
interface AuthTokenDetails {}
Frontend Standards
Component Rules
Critical Rules :
Never install UI components from npm
Build custom components only
Check existing components before creating new ones
// ✓ Correct - Custom component
export function Button ({ children , onClick } : ButtonProps ) {
return (
< button className = "bg-btnPrimary text-btnText px-4 py-2 rounded-lg" >
{ children }
</ button >
);
}
// ✗ Wrong - Using external component library
import { Button } from 'some-ui-library' ; // Never do this!
SWR Hook Rules
Critical : Each SWR hook must be in a separate function. Never use eslint-disable-next-line for hooks.
// ✓ Correct - Separate hooks
const usePosts = () => {
const fetch = useFetch ();
return useSWR ( 'posts' , () => fetch ( '/api/posts' ));
};
const useUsers = () => {
const fetch = useFetch ();
return useSWR ( 'users' , () => fetch ( '/api/users' ));
};
// ✗ Wrong - Multiple hooks in one function
const useData = () => {
return {
posts : () => useSWR ( 'posts' , getPosts ), // Wrong!
users : () => useSWR ( 'users' , getUsers ), // Wrong!
};
};
Fetch Hook Usage
// ✓ Correct - Use custom fetch hook
import { useFetch } from '@gitroom/helpers/utils/custom.fetch.tsx' ;
const fetch = useFetch ();
const data = await fetch ( '/api/endpoint' );
// ✗ Wrong - Using native fetch directly
const data = await fetch ( '/api/endpoint' ); // Don't use native fetch
Color System
All --color-custom* variables are deprecated. Use new color system.
// ✓ Correct - New color variables
< div className = "bg-newBgColor text-newTextColor" >
< button className = "bg-btnPrimary text-btnText" > Click </ button >
</ div >
// ✗ Wrong - Deprecated colors
< div className = "bg-customColor1 text-customColor2" > // Deprecated!
Component File Structure
'use client' ; // If client component
import { ReactNode } from 'react' ;
import clsx from 'clsx' ;
// Types/Interfaces first
interface ComponentNameProps {
children : ReactNode ;
variant ?: 'primary' | 'secondary' ;
className ?: string ;
}
// Component definition
export function ComponentName ({
children ,
variant = 'primary' ,
className
} : ComponentNameProps ) {
return (
< div className = { clsx ( 'base-classes' , className )} >
{ children }
</ div >
);
}
TypeScript Standards
Type Safety
// ✓ Correct - Proper types
interface User {
id : string ;
email : string ;
createdAt : Date ;
}
function getUser ( id : string ) : Promise < User > {
return db . user . findUnique ({ where: { id } });
}
// ✗ Wrong - Using 'any'
function getUser ( id : any ) : any { // Never use 'any'
return db . user . findUnique ({ where: { id } });
}
Avoid Type Assertions
// ✓ Correct - Type guards
function isUser ( obj : unknown ) : obj is User {
return typeof obj === 'object' && obj !== null && 'id' in obj ;
}
if ( isUser ( data )) {
console . log ( data . email ); // Type-safe
}
// ✗ Wrong - Type assertion
const user = data as User ; // Avoid unless absolutely necessary
Enums vs Union Types
// ✓ Preferred - Union types
type Status = 'draft' | 'published' | 'archived' ;
// ✓ Also acceptable - Enums (when needed)
enum Provider {
LOCAL = 'LOCAL' ,
GOOGLE = 'GOOGLE' ,
GITHUB = 'GITHUB' ,
}
Styling Standards
Tailwind CSS
// ✓ Correct - Tailwind classes
< div className = "flex items-center justify-between p-6 bg-newBgColor" >
// ✓ Correct - With clsx for conditionals
< div className = { clsx (
'base-class' ,
{ 'active-class' : isActive },
customClass
)} />
// ✗ Wrong - Inline styles
< div style = {{ display : 'flex' , padding : '24px' }} > // Avoid inline styles
SCSS Organization
Before writing components, check:
/apps/frontend/src/app/colors.scss
/apps/frontend/src/app/global.scss
/apps/frontend/tailwind.config.js
File Organization
Naming Conventions
Components: Button.tsx, UserProfile.tsx (PascalCase)
Services: auth.service.ts, user.service.ts (kebab-case)
Controllers: auth.controller.ts (kebab-case)
Utilities: custom.fetch.tsx, timer.ts (kebab-case)
Types: user.types.ts, api.types.ts (kebab-case)
Import Order
// 1. External libraries
import { Injectable } from '@nestjs/common' ;
import { User } from '@prisma/client' ;
// 2. Internal absolute imports
import { AuthService } from '@gitroom/backend/services/auth/auth.service' ;
import { DatabaseService } from '@gitroom/nestjs-libraries/database/database.service' ;
// 3. Relative imports
import { CreateUserDto } from './dto/create-user.dto' ;
import { UserMapper } from './user.mapper' ;
// 4. Type imports (if separated)
import type { AuthResponse } from './types' ;
Error Handling
Backend
// ✓ Correct - Proper error handling
try {
const user = await this . _userService . create ( dto );
return user ;
} catch ( error ) {
if ( error instanceof ConflictException ) {
throw error ;
}
throw new InternalServerErrorException ( 'Failed to create user' );
}
// ✗ Wrong - Silent failures
try {
const user = await this . _userService . create ( dto );
return user ;
} catch ( error ) {
// Empty catch - never do this!
}
Frontend
// ✓ Correct - Error states
const { data , error , isLoading } = useSWR ( 'posts' , fetchPosts );
if ( error ) return < ErrorState error ={ error } />;
if ( isLoading ) return < LoadingState />;
return < PostsList posts ={ data } />;
// ✗ Wrong - No error handling
const { data } = useSWR ( 'posts' , fetchPosts );
return < PostsList posts ={ data } />; // What if data is undefined?
Testing Standards
Test File Naming
auth.service.spec.ts # Unit tests
auth.controller.spec.ts # Unit tests
auth.e2e.spec.ts # E2E tests
Test Structure
describe ( 'AuthService' , () => {
let service : AuthService ;
let mockUserService : jest . Mocked < UserService >;
beforeEach ( async () => {
// Setup
});
describe ( 'register' , () => {
it ( 'should create a new user' , async () => {
// Arrange
const dto = { email: '[email protected] ' , password: 'password' };
// Act
const result = await service . register ( dto );
// Assert
expect ( result ). toHaveProperty ( 'jwt' );
});
it ( 'should throw error if email exists' , async () => {
// Test error case
});
});
});
Documentation Standards
/**
* Authenticates a user via OAuth provider
* @param provider - OAuth provider name (google, github, etc.)
* @param code - Authorization code from OAuth callback
* @returns Authentication tokens and user information
* @throws {UnauthorizedException} If authentication fails
*/
async authenticate (
provider : string ,
code : string
): Promise < AuthTokenDetails > {
// Implementation
}
JSDoc for Public APIs
Use JSDoc for all public APIs and exported functions.
Run Linting
# Lint all code (run from root only)
pnpm lint
# Fix auto-fixable issues
pnpm lint --fix
Linting can only be run from the root directory.
ESLint Configuration
Project uses:
@typescript-eslint for TypeScript
eslint-plugin-react for React
eslint-plugin-react-hooks for hooks rules
eslint-config-prettier for Prettier compatibility
Prettier
Prettier auto-formats on save (if configured in your editor).
Git Commit Standards
< type >( < scope > ) : < subject >
< body >
< footer >
Types:
feat: New feature
fix: Bug fix
docs: Documentation
refactor: Code refactoring
test: Tests
chore: Maintenance
Examples:
feat(integrations ): add Mastodon provider
fix(calendar ): resolve date rendering issue
docs(api ): update authentication guide
refactor(auth ): improve token refresh logic
Security Standards
Environment Variables
// ✓ Correct - Use environment variables
const apiKey = process . env . API_KEY ;
// ✗ Wrong - Hardcoded secrets
const apiKey = 'sk_live_123456' ; // Never commit secrets!
// ✓ Correct - Validate with DTO
import { IsEmail , IsString , MinLength } from 'class-validator' ;
export class CreateUserDto {
@ IsEmail ()
email : string ;
@ IsString ()
@ MinLength ( 8 )
password : string ;
}
// ✗ Wrong - No validation
function createUser ( email : string , password : string ) {
// Direct database insert without validation
}
Database Queries
// ✓ Correct - Select only needed fields
const user = await db . user . findUnique ({
where: { id },
select: { id: true , email: true , name: true },
});
// ✗ Wrong - Fetching all fields and relations
const user = await db . user . findUnique ({
where: { id },
include: { posts: true , comments: true , ... }, // Expensive!
});
// ✓ Correct - Memoize expensive computations
const sortedPosts = useMemo (
() => posts . sort (( a , b ) => b . createdAt - a . createdAt ),
[ posts ]
);
// ✓ Correct - Memoize callbacks
const handleClick = useCallback (() => {
doSomething ();
}, [ dependency ]);
Best Practices Summary
Use TypeScript
All code must be TypeScript. No any types.
Follow architecture
Backend: Controller → Service → Repository. Never skip layers.
Build custom components
Frontend: Never install UI components. Build custom.
Separate SWR hooks
Each SWR hook in its own function. No exceptions.
Use pnpm only
Never use npm or yarn. Only pnpm.
Test your code
Write tests for new features and bug fixes.
Document public APIs
Use JSDoc for exported functions and classes.
Run linting
Always run pnpm lint before committing.
Next Steps
Testing Strategy Learn testing best practices
Contributing Start contributing to Postiz