Adapters API Reference
Complete reference for the custom adapter system in @tgraph/backend-generator.
Prerequisites
Before using adapters, ensure these peer dependencies are installed in your project:
npm install express @types/express multer @types/multer @nestjs/common @nestjs/platform-express @nestjs/swagger
Getting Started
Import the adapter runtime from the package:
import { adapter } from '@tgraph/backend-generator/adapters';
import type { AdapterContext } from '@tgraph/backend-generator/adapters';
Important: Return your DTO directly from the handler. For direct responses (no service call), use adapter.response().
DTO Generation
The backend generator automatically extracts type definitions from your adapter files and generates corresponding DTO classes with proper validation decorators.
Supported Type Patterns
- Simple interfaces:
interface CreateDto { name: string; } - Type aliases:
type CreateDto = { name: string; } - Omit types:
type CreateDto = Omit<BaseDto, 'id'> - Pick types:
type CreateDto = Pick<BaseDto, 'name' | 'email'> - Intersection types:
type CreateDto = BaseDto & { extra: string } - Complex combinations:
type CreateDto = Omit<BaseDto, 'id'> & { slug: string }
Example
// Adapter file: src/features/project/adapters/create-with-slug.adapter.ts
import { CreateProjectInstanceDto } from '../create-projectInstance.dto';
type CreateProjectWithSlugDto = Omit<
CreateProjectInstanceDto,
'projectTypeId' | 'slug'
> & {
slug: string;
};
export default adapter.json<CreateProjectWithSlugDto, any, any, CreateProjectInstanceDto>(
{
method: 'POST',
path: '/project',
target: 'ProjectInstanceService.create',
},
async (ctx) => {
const slug = ctx.helpers.slugify(ctx.body.name);
return {
...ctx.body,
slug,
projectTypeId: 'cmhdupp4y0001luv07mgfu7pr',
};
},
);
The generator will automatically create a DTO class with all properties from CreateProjectWithSlugDto, including proper validation decorators:
// Generated: create-project-with-slug-input.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsString, IsNumber, IsBoolean, IsArray } from 'class-validator';
/**
* Input DTO for CreateProjectWithSlug adapter
* Generated from type: CreateProjectWithSlugDto
*/
export class CreateProjectWithSlugInputDto {
@ApiProperty({ type: 'string' })
@IsString()
name: string;
@ApiProperty({ type: 'string', required: false })
@IsOptional()
@IsString()
description?: string;
@ApiProperty({ type: 'string' })
@IsString()
slug: string;
}
Factory Functions
adapter.json()
Create a JSON-based adapter endpoint.
adapter.json(config: AdapterConfig, handler: AdapterHandler): AdapterFactoryResult
Parameters:
config- Configuration object for the endpointhandler- Async function that processes the request
Returns: AdapterFactoryResult with type 'json'
Example:
import { adapter } from '@tgraph/backend-generator/adapters';
export default adapter.json(
{
method: 'POST',
path: '/custom',
target: 'UserService.create',
auth: 'JwtAuthGuard',
},
async (ctx) => {
return ctx.body; // Return DTO directly
},
);
adapter.multipart()
Create a multipart/form-data adapter endpoint for file uploads.
adapter.multipart(config: AdapterConfig, handler: AdapterHandler): AdapterFactoryResult
Parameters:
config- Configuration object for the endpointhandler- Async function that processes the request
Returns: AdapterFactoryResult with type 'multipart'
Example:
import { adapter } from '@tgraph/backend-generator/adapters';
export default adapter.multipart(
{
method: 'POST',
path: '/upload',
target: 'UserService.update',
},
async (ctx) => {
const file = Array.isArray(ctx.files) ? ctx.files[0] : ctx.files;
const url = await ctx.helpers.upload.minio(file);
return { avatarUrl: url }; // Return DTO directly
},
);
adapter.response()
Create a direct HTTP response without calling a service method.
adapter.response(
status: number,
body: any,
headers?: Record<string, string>
): AdapterDirectResponse
Parameters:
status- HTTP status code (e.g., 200, 201, 400)body- Response body (will be JSON-serialized)headers- Optional custom response headers
Returns: AdapterDirectResponse object
Example:
import { adapter } from '@tgraph/backend-generator/adapters';
export default adapter.json(
{
method: 'POST',
path: '/webhook',
},
async (ctx) => {
// Process webhook
await processWebhook(ctx.body);
return adapter.response(
201,
{
success: true,
message: 'Created successfully',
},
{
'X-Request-Id': ctx.helpers.uuid(),
},
);
},
);
Configuration Types
AdapterConfig
Configuration object for adapter endpoints.
interface AdapterConfig {
method: HttpMethod; // Required
path: string; // Required
target?: string | null; // Optional
auth?: string | string[]; // Optional
select?: string[]; // Optional
include?: string[]; // Optional
description?: string; // Optional
summary?: string; // Optional
tags?: string[]; // Optional
}
Fields:
method(required) - HTTP method:'GET','POST','PUT','DELETE', or'PATCH'path(required) - Route path relative to controller base (must start with/)target- Service method to call in formatServiceName.methodName, ornullto bypassauth- Guard name(s) to apply for authentication/authorizationselect- Array of field names to include in response (Prisma select)include- Array of relation names to include in response (Prisma include)description- Description for OpenAPI documentationsummary- Summary for OpenAPI documentationtags- Tags for OpenAPI documentation
Example:
{
method: 'POST',
path: '/with-validation',
target: 'UserService.createOne',
auth: ['JwtAuthGuard', 'AdminGuard'],
select: ['id', 'email', 'createdAt'],
summary: 'Create user with validation',
description: 'Creates a new user after custom validation',
tags: ['users', 'admin'],
}
HttpMethod
Allowed HTTP methods.
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
AdapterType
Type of adapter.
type AdapterType = 'json' | 'multipart';
Context Types
AdapterContext
Context object passed to adapter handlers.
interface AdapterContext<TBody = any, TQuery = any, TParams = any> {
url: string;
params: TParams;
query: TQuery;
headers: Record<string, string | string[] | undefined>;
body: TBody;
files?: Express.Multer.File | Express.Multer.File[];
user?: any;
req: Request;
res: Response;
di: AdapterDI;
helpers: AdapterHelpers;
}
Fields:
url- Full request URLparams- Route parameters (e.g.,{ id: '123' })query- Query string parameters (e.g.,{ page: '1', limit: '10' })headers- Request headersbody- Parsed request body (JSON or form data)files- Uploaded files (multipart adapters only)user- Authenticated user object (from guard/strategy)req- Raw Express Request objectres- Raw Express Response objectdi- Dependency injection containerhelpers- Utility functions
Example:
async (ctx: AdapterContext) => {
const userId = ctx.params.id;
const page = parseInt(ctx.query.page || '1');
const email = ctx.body.email;
const token = ctx.headers.authorization;
const currentUser = ctx.user;
return { args: { userId, email } };
};
AdapterDI
Dependency injection container.
interface AdapterDI {
prisma: PrismaService;
[key: string]: any;
}
Fields:
prisma- PrismaService instance for database access[key]- Additional injected services (user-configurable)
Example:
async (ctx) => {
// Access Prisma
const user = await ctx.di.prisma.user.findUnique({
where: { id: ctx.params.id },
});
// Access custom services (if configured)
await ctx.di.emailService.send({ to: user.email });
return { args: user };
};
AdapterHelpers
Utility functions available in context.
interface AdapterHelpers {
uuid(): string;
slugify(text: string): string;
ext(filename: string): string;
pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>;
assert(condition: any, message?: string): asserts condition;
upload: {
minio?(file: Express.Multer.File, bucket?: string): Promise<string>;
local?(file: Express.Multer.File, directory?: string): Promise<string>;
[key: string]: any;
};
}
Methods:
uuid()
Generate a UUID v4.
uuid(): string
Returns: UUID string
Example:
const id = ctx.helpers.uuid();
// "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
slugify()
Convert string to URL-friendly slug.
slugify(text: string): string
Parameters:
text- Text to slugify
Returns: Slugified string
Example:
const slug = ctx.helpers.slugify('Hello World! 2024');
// "hello-world-2024"
ext()
Extract file extension from filename.
ext(filename: string): string
Parameters:
filename- Filename to extract extension from
Returns: Extension without dot
Example:
const extension = ctx.helpers.ext('document.pdf');
// "pdf"
pick()
Select specific properties from an object.
pick<T extends object, K extends keyof T>(obj: T, keys: K[]): Pick<T, K>
Parameters:
obj- Source objectkeys- Array of keys to pick
Returns: New object with only specified keys
Example:
const user = { id: '1', email: 'user@example.com', password: 'hash' };
const safe = ctx.helpers.pick(user, ['id', 'email']);
// { id: '1', email: 'user@example.com' }
assert()
Assert a condition, throw BadRequestException if false.
assert(condition: any, message?: string): asserts condition
Parameters:
condition- Condition to assertmessage- Error message if assertion fails
Throws: BadRequestException if condition is falsy
Example:
ctx.helpers.assert(ctx.body.age >= 18, 'Must be 18 or older');
ctx.helpers.assert(ctx.body.email, 'Email is required');
upload
Upload utilities (user-implemented).
upload: {
minio?(file: Express.Multer.File, bucket?: string): Promise<string>;
local?(file: Express.Multer.File, directory?: string): Promise<string>;
[key: string]: any;
}
Note: These are placeholders. Implement in src/adapters/helpers.ts:
// src/adapters/helpers.ts
import { Minio } from 'minio';
helpers.upload.minio = async (file, bucket = 'default') => {
const client = new Minio.Client({
endPoint: process.env.MINIO_ENDPOINT,
accessKey: process.env.MINIO_ACCESS_KEY,
secretKey: process.env.MINIO_SECRET_KEY,
});
const filename = `${uuid()}-${file.originalname}`;
await client.putObject(bucket, filename, file.buffer);
return `https://cdn.example.com/${bucket}/${filename}`;
};
Result Types
AdapterServiceCallResult
Result when calling a service method.
interface AdapterServiceCallResult {
args: any;
}
Fields:
args- Arguments to pass to the target service method
Example:
return {
args: {
email: ctx.body.email,
password: hashedPassword,
},
};
AdapterDirectResponse
Result when bypassing service and responding directly.
interface AdapterDirectResponse {
status: number;
body: any;
headers?: Record<string, string>;
__isDirectResponse: true;
}
Fields:
status- HTTP status codebody- Response bodyheaders- Optional custom headers__isDirectResponse- Internal marker (do not set manually)
Note: Use adapter.response() to create this type.
Example:
return adapter.response(200, {
success: true,
processedAt: new Date(),
});
AdapterResult
Union of possible return types.
type AdapterResult = AdapterServiceCallResult | AdapterDirectResponse;
AdapterHandler
Handler function signature.
type AdapterHandler<TBody = any, TQuery = any, TParams = any> = (
context: AdapterContext<TBody, TQuery, TParams>,
) => Promise<AdapterResult>;
Parameters:
context- Adapter context with request data and utilities
Returns: Promise resolving to either service call result or direct response
Example:
const handler: AdapterHandler = async (ctx) => {
if (ctx.body.skipService) {
return adapter.response(200, { skipped: true });
}
return { args: ctx.body };
};
Parser Types
AdapterDefinition
Parsed adapter file structure (internal).
interface AdapterDefinition {
filePath: string;
name: string;
type: AdapterType;
config: AdapterConfig;
handlerCode: string;
inputDtoName?: string;
outputType?: string;
}
Note: This type is primarily used internally by the generator.
Advanced Usage
Type-Safe Handlers
Use TypeScript generics for type safety:
interface CreateUserBody {
email: string;
password: string;
age: number;
}
interface UserParams {
id: string;
}
export default adapter.json(
{
method: 'POST',
path: '/typed',
target: 'UserService.create',
},
async (ctx: AdapterContext<CreateUserBody, any, UserParams>) => {
// ctx.body is typed as CreateUserBody
// ctx.params is typed as UserParams
const email: string = ctx.body.email; // Type-safe
const userId: string = ctx.params.id; // Type-safe
return { args: ctx.body };
},
);
Custom DI Services
Extend the DI container with custom services:
// In adapter context builder (src/adapters/context.ts)
export class AdapterContextBuilder {
constructor(
private readonly prisma: PrismaService,
private readonly emailService: EmailService,
private readonly stripeService: StripeService,
) {}
build(req: Request, res: Response, files?: any): AdapterContext {
return {
// ... other fields
di: {
prisma: this.prisma,
emailService: this.emailService,
stripeService: this.stripeService,
},
// ... other fields
};
}
}
Then use in adapters:
async (ctx) => {
await ctx.di.emailService.send({ to: ctx.body.email });
await ctx.di.stripeService.charge({ amount: ctx.body.amount });
return { args: ctx.body };
};
Error Handling
Throw standard NestJS exceptions:
import {
BadRequestException,
UnauthorizedException,
ForbiddenException,
NotFoundException,
ConflictException,
InternalServerErrorException,
} from '@nestjs/common';
export default adapter.json(
{
method: 'POST',
path: '/with-errors',
},
async (ctx) => {
if (!ctx.body.email) {
throw new BadRequestException('Email is required');
}
if (!ctx.user) {
throw new UnauthorizedException('Authentication required');
}
if (ctx.user.role !== 'admin') {
throw new ForbiddenException('Admin access required');
}
const existing = await ctx.di.prisma.user.findUnique({
where: { email: ctx.body.email },
});
if (existing) {
throw new ConflictException('Email already exists');
}
return { args: ctx.body };
},
);
Validation
The generator creates DTOs automatically, but you can also create custom DTOs:
Custom DTO
Create alongside your adapter:
// src/features/user/adapters/create-validated-input.dto.ts
import { IsEmail, IsString, MinLength, IsInt, Min } from 'class-validator';
export class CreateValidatedInputDto {
@IsEmail()
email: string;
@IsString()
@MinLength(8)
password: string;
@IsInt()
@Min(18)
age: number;
}
Reference in generated controller (or regenerate after creating DTO).
Generator Integration
Discovery Process
- Generator scans
{modulePath}/adapters/*.adapter.ts - Parser extracts configuration and handler code
- Validator checks configuration and field references
- DTO generator creates input DTOs (if needed)
- Controller generator injects endpoints
- OpenAPI generator adds documentation
File Structure
src/
└── features/
└── user/
├── adapters/
│ ├── create-with-password.adapter.ts
│ ├── upload-avatar.adapter.ts
│ └── custom-endpoint.adapter.ts
├── user.module.ts
├── user.tg.controller.ts # Adapters injected here
└── user.tg.service.ts
Generated Code
The generator adds to your controller:
@Controller('users')
export class UserTgController {
constructor(
private readonly userTgService: UserTgService,
private readonly prisma: PrismaService, // Added for adapters
) {}
// ... standard CRUD endpoints ...
// Adapter endpoint (generated)
@Post('/with-password')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Create user with hashed password' })
@ApiResponse({ status: 201 })
async createWithPassword(
@Body() body: CreateWithPasswordInputDto,
@Query() query: any,
@Param() params: any,
@Req() req: Request,
@Res({ passthrough: true }) res: Response,
) {
const contextBuilder = new AdapterContextBuilder(this.prisma);
const context = contextBuilder.build(req, res);
const adapterModule = await import('./adapters/create-with-password.adapter');
const result = await adapterModule.default.handler(context);
if ('__isDirectResponse' in result && result.__isDirectResponse) {
res.status(result.status);
if (result.headers) {
for (const [key, value] of Object.entries(result.headers)) {
res.setHeader(key, value);
}
}
return result.body;
}
const serviceResult = await this.userTgService.create(result.args);
return { data: serviceResult };
}
}
See Also
- Custom Adapters Guide - Comprehensive usage guide
- Custom Endpoints Recipe - Practical examples
- Authentication Guards - Securing adapters