Aller au contenu

Code Structure

This guide explains how the Plus261 codebase is organized and how to navigate it effectively.

By the end of this guide, you’ll understand:

  • ✅ The monorepo structure and organization
  • ✅ How modules are structured and organized
  • ✅ The different types of modules (CRUD, custom resolvers, services)
  • ✅ How to create a new module
  • ✅ Naming conventions and import patterns
  • ✅ Best practices for code organization
What You Need Where to Find It
📁 API Code apps/api/src/
🌐 Web Code apps/web/src/
📊 Database Schema libs/db/src/schema/
📚 Shared Libraries libs/
📖 API Documentation /api/*
🛠️ Type GraphQL Query Docs /type-graphql-query/*

The project follows a monorepo structure with multiple applications and libraries:

plus261/
├── apps/
│ ├── api/ → GraphQL API server
│ ├── web/ → Web client application
│ └── documentation/ → Documentation site
├── libs/
│ ├── type-graphql-query/ → GraphQL schema builder
│ ├── drizzle-query/ → Database query layer
│ ├── db/ → Database schema
│ └── ... → Other shared libraries
└── packages/ → Shared packages

The API (apps/api/src/) is organized into modules, where each module represents a domain entity or feature.

Each module follows a consistent structure:

module-name/
├── dto/ → Data Transfer Objects
│ ├── module-name.type.ts → GraphQL output type
│ ├── module-name.input.ts → GraphQL input type (optional)
│ └── module-name-aggregate.type.ts → Aggregate type (optional)
├── module-name.module.ts → Module configuration
├── module-name.resolver.ts → Custom resolvers (optional)
└── module-name.service.ts → Business logic (optional)
product/
├── dto/
│ ├── product.type.ts → Product GraphQL type
│ ├── product.input.ts → Product input for mutations
│ └── product-aggregate.type.ts → Product aggregation type
└── product.module.ts → Product module configuration

Most modules are standard CRUD modules that use Type GraphQL Query to automatically generate resolvers.

Example: product.module.ts

import { Permission, TypeGraphqlQueryOptions } from '@lib/type-graphql-query';
import { Product } from './dto/product.type';
import { ProductInput } from './dto/product.input';
import { product as productTable } from '@lib/db';
const resolvers = [
{
enableSubscriptions: true,
DTOClass: Product,
UpsertDTOClass: ProductInput,
EntityClass: productTable,
enableTotalCount: true,
enableAggregate: true,
read: {
authorize: Permission('product.view'),
},
create: {
authorize: Permission('product.create'),
},
// ... other operations
},
] satisfies TypeGraphqlQueryOptions['resolvers'];
export default resolvers;

Some modules have custom resolvers for complex business logic.

Example: user/user.resolver.ts

import { Resolver, Query } from '@lib/type-graphql';
import { User } from './dto/user.type';
@Resolver(() => User)
export class UserResolver {
@Query(() => User)
async me(@Ctx() context: Context): Promise<User> {
// Custom logic
}
}

Modules can have service classes for complex business logic.

Example: user/user.service.ts

export class UserService {
async findUserById(id: string): Promise<User> {
// Business logic
}
}

GraphQL output types define what data is returned from queries.

import { ObjectType, ID } from '@lib/type-graphql';
import { EnhancedField, IDField, Relation } from '@lib/type-graphql-query';
@ObjectType()
export class Product {
@IDField(() => ID)
id: string;
@EnhancedField(() => String)
name: string;
@Relation('category', () => Category)
category: Category;
}

Input types define what data is accepted for mutations.

import { InputType, Field } from '@lib/type-graphql';
@InputType()
export class ProductInput {
@Field(() => String)
name: string;
@Field(() => String, { nullable: true })
description?: string;
}

All modules are registered in create-schema.ts:

import productModuleResolvers from './product/product.module';
const { resolvers, typeDefs } = await buildTypeDefsAndResolvers({
resolvers: [
...productModuleResolvers,
// ... other modules
],
// ... other options
});
  • auth/ - Authentication and authorization
  • acl/ - Access control lists
  • user/ - User management
  • organization/ - Organization management
  • product/ - Product management
  • order/ - Order management
  • order-line/ - Order line items
  • category/ - Product categories
  • storage/ - Stock management
  • file/ - File management
  • activity/ - Activity tracking
  • geolocalization/ - Geolocation services
  • logging/ - Logging system
  • metrics/ - Metrics collection
  • utils/ - Utility functions
  • services/ - Shared services
  • dataloader/ - DataLoader utilities
  • Modules: module-name.module.ts
  • Resolvers: module-name.resolver.ts
  • Services: module-name.service.ts
  • Types: module-name.type.ts
  • Inputs: module-name.input.ts
  • Aggregates: module-name-aggregate.type.ts
  • Use kebab-case for directory names: order-line/, product-image/
  • Use singular for module directories: product/, order/, user/
  • Use PascalCase for class names: Product, UserResolver, OrderService
  • Use camelCase for variables and functions: getUserById, productService

Use absolute imports with path aliases:

import { Product } from '../product/dto/product.type';
import { db } from '../database';
import { Permission } from '@lib/type-graphql-query';
  1. External libraries
  2. Internal libraries (@lib/...)
  3. Relative imports (./..., ../...)

Example:

import { ObjectType, ID } from '@lib/type-graphql';
import { EnhancedField, IDField } from '@lib/type-graphql-query';
import { product as productTable } from '@lib/db';
import { Category } from '../category/dto/category.type';
import { metadataColumns } from '../utils/metadata-columns';

Follow these steps to create a new module:

Fenêtre de terminal
mkdir -p apps/api/src/new-module/dto

Create dto/new-module.type.ts:

import { ObjectType, ID } from '@lib/type-graphql';
import { EnhancedField, IDField } from '@lib/type-graphql-query';
@ObjectType()
export class NewModule {
@IDField(() => ID)
id: string;
@EnhancedField(() => String)
name: string;
}

Create dto/new-module.input.ts:

import { InputType, Field } from '@lib/type-graphql';
@InputType()
export class NewModuleInput {
@Field(() => String)
name: string;
}

Create new-module.module.ts:

import { Permission, TypeGraphqlQueryOptions } from '@lib/type-graphql-query';
import { NewModule } from './dto/new-module.type';
import { NewModuleInput } from './dto/new-module.input';
import { newModule as newModuleTable } from '@lib/db';
const resolvers = [
{
DTOClass: NewModule,
UpsertDTOClass: NewModuleInput,
EntityClass: newModuleTable,
read: {
authorize: Permission('new-module.view'),
},
create: {
authorize: Permission('new-module.create'),
},
update: {
authorize: Permission('new-module.update'),
},
delete: {
authorize: Permission('new-module.delete'),
},
},
] satisfies TypeGraphqlQueryOptions['resolvers'];
export default resolvers;

Add to create-schema.ts:

import newModuleResolvers from './new-module/new-module.module';
const { resolvers, typeDefs } = await buildTypeDefsAndResolvers({
resolvers: [
...newModuleResolvers,
// ... other modules
],
});

Each module should represent a single domain entity or feature.

Prefer using Type GraphQL Query for standard CRUD operations instead of writing custom resolvers.

  • DTOs - Data structure
  • Modules - Configuration
  • Resolvers - Custom GraphQL operations
  • Services - Business logic

Use utilities like metadataColumns for common field patterns:

import metadataColumns from '../utils/metadata-columns';
create: {
...metadataColumns,
authorize: Permission('product.create')
}

Add comments for complex business logic or non-obvious decisions.

Task Documentation
Create a new module Creating a New Module
Add a GraphQL query Type GraphQL Query - Queries
Add authentication API - Authentication
Understand permissions API - Permissions
Debug an issue Debugging Guide

Continue your learning journey:

  1. 📝 Code Style - Learn coding conventions
  2. 🛠️ Development Guide - Understand the development workflow
  3. 🎯 Type GraphQL Query - Master the schema builder
  4. 🧪 Testing Guide - Learn how to write tests