# Code Example

```typescript

// ✅ Import framework utilities and helpers
import {
  DateString,
  AppContext,
  useResponseSchemas,
  ResponseBadRequest,
  ResponseErrorSchema,
  ResponseInternalServerError,
  ResponseForbidden,
  response400,
  responseForbidden,
  responseNull,
  response200
} from "@tsdiapi/server"; // Core routing and types
import { Type } from "@sinclair/typebox"; // For schema definitions
import { JWTGuard, useSession } from "@tsdiapi/jwt-auth"; // Auth guard and session access
import { Container } from "typedi"; // DI container
import { useS3Provider } from "@tsdiapi/s3"; // S3 provider

// Use this import for session type, this file is generated by the jwt-auth plugin 
import { UserSession } from "@base/api/auth/auth.service.js";

// Use @generated/typebox-schemas/models/index.js models for responses, request bodies and query parameters
// Use this import for responses
// Output to response
import { OutputContactSchema } from "@generated/typebox-schemas/models/index.js";
// Input to request body
import { InputContactSchema } from "@generated/typebox-schemas/models/index.js";
// Query to query parameters in list routes
import { QueryListContactSchema, OutputListContactSchema } from "@generated/typebox-schemas/models/index.js";

import { usePrisma } from "@tsdiapi/prisma";
// Use this import for prisma client
import type { PrismaClient } from "@generated/prisma/index.js";
// Use this function to get prisma client but inside the service method (not outside the function) and pass the prisma client type: usePrisma<PrismaClient>()
import { usePrisma } from "@tsdiapi/prisma";

// ✅ Import service
import { ContactService } from "./contacts.service.js";

// 🔧 Generic reusable schema for all API error messages
// RULE: Use error schema to add details to the error response
const ErrorDetailsSchema = Type.Object({
  errors: Type.Optional(Type.Array(Type.Object({
    message: Type.String()
  })))
});

// Add file-related schemas
const OutputUploadSchema = Type.Object({
  url: Type.String(),
  key: Type.String(),
  bucket: Type.String(),
  region: Type.String()
});

// Simple schema with one regular field and one file
import { DateString } from "@tsdiapi/server"; // RULE: Use DateString for date fields!
const InputUploadSchema = Type.Object({
  description: Type.String(),
  dateField: DateString(),
  // RULE: Use Type.String({ format: "binary" }) for file fields
  file: Type.String({ format: "binary" })
});

// Multiple files schema
const InputMultiFilesSchema = Type.Object({
  files: Type.Array(Type.String({ format: "binary" }))
});
// or
const InputWithFilesSchema = Type.Object({
  photo: Type.String({ format: "binary" }),
  document: Type.String({ format: "binary" }),
  // and more fields
});

// ─────────────────────────────────────────────────────────────
// Main module export — loaded automatically by the framework
// File: src/api/contacts/contacts.module.ts
// ─────────────────────────────────────────────────────────────
export default function ContactsModule({ useRoute }: AppContext): void {
  // 🧩 Dependency Injection — retrieve service instance via TypeDI
  const contactService = Container.get(ContactService);

  // Setup response schemas for the module
  const { codes, sendError, sendSuccess, send } = useResponseSchemas(
    OutputContactSchema,
    ErrorDetailsSchema
  );

  // ─────────────────────────────────────────────────────────────
  // GET /contacts — List contacts with optional filters
  // RULES:
  // 1. Every route MUST register ALL possible response codes
  // 2. Success codes use 200 for all operations (including POST)
  // 3. Error codes (400, 401, 403, 404, 500) must be documented
  // 4. For authenticated routes, 403 response is REQUIRED
  // 5. Each code MUST have a corresponding schema
  // ─────────────────────────────────────────────────────────────
  useRoute()
    .controller("contacts") // RULE: Use controller name
    .get("/") // RULE: Use HTTP method and path
    .version("1") // RULE: Always specify API version
    .codes(codes) // Use the predefined response codes
    .summary("List all user contacts") // RULE: Use a meaningful summary, swagger will use it as a description
    .tags(["Contacts"]) // RULE: Use tags for grouping, swagger will use it for grouping
    .auth("bearer") // RULE: Use auth method and guard (import from @tsdiapi/jwt-auth)
    .guard(JWTGuard()) // RULE: Use guard for authentication (import from @tsdiapi/jwt-auth)
    // Then we define the query parameters using QueryListContactSchema
    .query(QueryListContactSchema)
    // Then we define the route handler, we can use useSession for type-safe session access
    .handler(async (req) => {
        try {
            // RULE: Use useSession for type-safe session access
            const session = useSession<UserSession>(req);
            const { items, total } = await contactService.listContacts(session.id, req.query);
            // We need to return the data with status code 200 registered in the code() method above
            return sendSuccess({
                items: items,
                total: total,
                skip: req.query.skip,
                take: req.query.take
            });
        } catch (error) {
            // RULE: Check if error is ResponseError, otherwise return 400 error with message
            return error instanceof ResponseError ? error : sendError("Failed to list contacts", {
              // Not required, we can use it for add details to the error response
              errors: [{
                message: "Failed to list contacts"
              }]
            });
        }
    })
    // We need to build the route, this is required for the route to be registered!
    .build();

  // ─────────────────────────────────────────────────────────────
  // POST /contacts — Create a contact
  // RULES:
  // 1. Use Input schemas for request bodies
  // 2. Success code is 200 (not 201)
  // 3. Always validate user ownership
  // ─────────────────────────────────────────────────────────────
  useRoute()
    .controller("contacts")
    .put("/:id") // RULE: Use HTTP method and path with dynamic route (:id), we need to define the params for the dynamic route see below
    .version("1")
    .codes(buildExtraResponseCodes(OutputUploadSchema)) // Use the predefined response codes
    // We need to define the params for the dynamic route (:id), we can use req.params for them
    .params(Type.Object({ id: Type.String() }))
    .summary("Create contact")
    .tags(["Contacts"])
    // We can use auth method and guard, we can use async function for custom validation
    .auth('bearer', async (req, reply) => {
        const isValid = await isBearerValid(req);
        if (!isValid) {
          // Error without payload (details in the error schema)
            return responseForbidden('Invalid access token');
        }
        return true;
    })
    // Use query parameters for optional fields
    .query(Type.Object({
      isPrivate: Type.Boolean()
    }))
    // Required for file fields
    .acceptMultipart() // RULE: Use acceptMultipart() for file fields
    .body(InputUploadSchema) // RULE: Use Input schema for request body
    // RULE: Use fileOptions for file fields validation
    .fileOptions(
      {
        maxFileSize: 10 * 1024 * 1024, // 10MB
        accept: ["image/*", "application/pdf"]
      },
      "file" // Not required, we can use it for set validation rules for the specific file field
    )
    .handler(async (req) => {
      try {
        const params = req.params; // RULE: Use req.params for dynamic route params
        const body = req.body; // RULE: Use req.body for request body
        const { isPrivate } = req.query; // RULE: Use req.query for query parameters
        // RULE: Use useSession for type-safe session access
        const session = useSession<{ userId: string }>(req);
        // RULE: Use req.tempFiles for file fields, this a temp file from the client, we need to save it to the storage and get the url
        const file = req.tempFiles[0];
        // use import { useS3Provider } from "@tsdiapi/s3";
        const s3provider = useS3Provider();
        // Manual upload
        const upload = await s3provider.uploadToS3({
            buffer: file.buffer,
            mimetype: file.mimetype,
            originalname: file.filename
        }, isPrivate);
        return response200(upload); // or responseSuccess(upload)
      } catch (error) {
        // RULE: Check if error is ResponseError, otherwise return 400 error with message
        return error instanceof ResponseError ? error : response400("Failed to upload file");
      }
    })
    .build();
  
  /*
  *   Use InputContactSchema for request body
  */
  useRoute()
    .controller("contacts")
    .post("/")
    .version("1")
    .code(200, OutputContactSchema)
    .code(400, ResponseErrorSchema)
    .body(InputContactSchema)
    .handler(async (req) => {
        try {
            const contact = await contactService.create(req.body);
            return response200(contact);
        } catch (error) {
            return error instanceof ResponseError ? error : response400("Failed to create contact");
        }
    })
    .build();



  // ─────────────────────────────────────────────────────────────
  // DELETE /contacts/:id — Delete a contact
  // RULES:
  // 1. Use resolve() for existence check
  // 2. Return 204 for successful deletion
  // 3. Always check ownership before deletion
  // ─────────────────────────────────────────────────────────────

  useRoute()
    .controller("contacts")
    .delete(":id")
    .version("1")
    .code(204, Type.Null())
    .code(400, ResponseErrorSchema)
    .code(403, ResponseErrorSchema)
    .summary("Delete contact")
    .tags(["Contacts"])
    .auth("bearer")
    // use custom guard for custom validation
    .guard(
      async (req, reply) => {
        const isValid = await isBearerValid(req);
        if (!isValid) {
          return responseForbidden('Invalid access token');
        }
        return true;
      }
    )
    .params(Type.Object({ id: Type.String() }))
    // Resolver acting as a guard - should throw errors
    .resolve(async (req) => {
      const contact = await contactService.getContactById(req.params.id);
      if (!contact) {
        throw new ResponseBadRequest("Contact not found");
      }
      return contact;
    })
    .handler(async (req) => {
      try {
        const session = useSession<{ userId: string }>(req);
        const contact = req.routeData;

        // RULE: Always check ownership
        if (contact.userId !== session.userId) {
          throw new ResponseForbidden("Forbidden");
        }

        await contactService.deleteContact(req.params.id);
        return responseNull();
      } catch (error) {
        return error instanceof ResponseError ? error : response400("Failed to delete contact");
      }
    })
    .build();
```

```typescript
// Service implementation example
import { Service } from "typedi";
import {
  ResponseBadRequest,
  ResponseForbidden,
} from "@tsdiapi/server";


// Use this import for prisma types
import { Prisma } from '@generated/prisma/index.js';
// Use this import for prisma client type
import type { PrismaClient } from "@generated/prisma/index.js";
// Use this import for prisma client
import { usePrisma } from "@tsdiapi/prisma";
// Use this import for input schemas types
import { InputContactSchemaType } from "@generated/typebox-schemas/models/index.js";
// Use this import for session type, this file is generated by the jwt-auth plugin 
import { UserSession } from '@base/api/auth/auth.service.js';

@Service()
export class ContactService {
  // Don't use prisma client directly, use usePrisma() inside methods
  async findById(id: string) {
    const prisma = usePrisma<PrismaClient>();
    const contact = await prisma.contact.findUnique({ where: { id } });
    if (!contact) {
      throw new ResponseBadRequest(`Contact with ID ${id} not found`);
    }
    return contact;
  }

  async create(data: InputContactSchemaType) {
    const prisma = usePrisma<PrismaClient>();
    try {
      return await prisma.contact.create({ data });
    } catch (error) {
      throw new ResponseBadRequest("Failed to create contact");
    }
  }

  async update(session: UserSession, id: string, data: InputContactSchemaType) {
    const prisma = usePrisma<PrismaClient>();
    const contact = await this.findById(id);

    // Always check ownership
    if (contact.userId !== session.id) {
      throw new ResponseForbidden(
        "You do not have permission to update this contact"
      );
    }

    try {
      return await prisma.contact.update({
        where: { id },
        data,
      });
    } catch (error) {
      throw new ResponseBadRequest("Failed to update contact");
    }
  }

  async delete(session: UserSession, id: string) {
    const prisma = usePrisma<PrismaClient>();
    const contact = await this.findById(id);

    // Always check ownership
    if (contact.userId !== session.id) {
      throw new ResponseForbidden(
        "You do not have permission to delete this contact"
      );
    }

    try {
      await prisma.contact.delete({ where: { id } });
    } catch (error) {
      throw new ResponseBadRequest("Failed to delete contact");
    }
  }

  async listContacts(session: UserSession, query: QueryListContactSchemaType) {
    const prisma = usePrisma<PrismaClient>();
    const where: Prisma.ContactWhereInput = {
        userId: session.id,
        // Date range filtering
        ...(query.dateAtLte || query.dateAtGte ? {
            createdAt: {
                ...(query.dateAtGte && { gte: query.dateAtGte }),
                ...(query.dateAtLte && { lte: query.dateAtLte })
            }
        } : {}),

        // Search filtering
        ...(query?.search ? {
            OR: [
                { name: { contains: query.search, mode: "insensitive" } },
                { email: { contains: query.search, mode: "insensitive" } },
                { phone: { contains: query.search, mode: "insensitive" } }
            ]
        } : {})
    };

    // Get paginated results
    const results = await prisma.contact.findMany({
        take: query.take || 100,
        skip: query.skip || 0,
        ...(query?.orderBy ? {
            orderBy: {
                [query.orderBy]: query.orderDirection || 'asc'
            }
        } : {}),
        where: where
    });

    // Get total count for pagination
    const total = await prisma.contact.count({
        where: where
    });

    return {
        items: results,
        total: total
    };
  }
}
```

```typescript
// ─────────────────────────────────────────────────────────────
// API DEVELOPMENT RULES AND BEST PRACTICES
// ─────────────────────────────────────────────────────────────

/* 1. DATE HANDLING
Always use DateString from @tsdiapi/server for date fields:
Correct: createdAt: DateString()
Incorrect: createdAt: Type.String({ format: "date-time" })
*/

/* 2. FILE HANDLING
Always use Type.String({ format: "binary" }) for file fields:
Correct: file: Type.String({ format: "binary" })
Incorrect: file: Type.String()
- Use .acceptMultipart() for file uploads
- Define fileOptions with size limits and accepted types
- Access files via req.tempFiles
*/

/* 3. RESPONSE CODES
Every route MUST register ALL possible response codes. There are several ways to define response codes:

A. Using individual code() calls:
.code(200, OutputSchema) // Success response
.code(400, ErrorSchema)  // Bad request
.code(401, ErrorSchema)  // Unauthorized
.code(403, ErrorSchema)  // Forbidden
.code(404, ErrorSchema)  // Not found
.code(500, ErrorSchema)  // Internal server error

B. Using codes() with predefined codes object:
const { codes } = useResponseSchemas(OutputSchema, ErrorSchema);
.codes(codes) // Will include all standard codes

or use manual codes
.codes({200: OutputSchema, 400: ErrorSchema, 403: ErrorSchema, 404: ErrorSchema, 500: ErrorSchema}) 

C. Using response builders:
.code(200, Type.Null()) // For 204 No Content
.code(400, ResponseBadRequest)
.code(403, ResponseForbidden)
.code(500, ResponseInternalServerError)

Rules for response codes:
- Success codes use 200 for all operations (including POST)
- Error codes (400, 401, 403, 404, 500) must be documented
- Each code MUST have a corresponding schema
- All possible error scenarios MUST be documented
- For authenticated routes, 403 response code is REQUIRED
- Use appropriate response builders for common error cases
*/

/* 4. SESSION HANDLING
Session object contains all data from JWT token:
const token = await authProvider.signIn({
  userId: "123",
  role: "admin",
  permissions: ["read", "write"]
});
Access in route handler:
const { userId, role, permissions } = req.session;
For type-safe access, use useSession:
const session = useSession<UserSession>(req);
*/

/* 5. ERROR HANDLING
There are several approaches to error handling in TSDIAPI:

1. Using Response Schemas (Recommended):
const { codes, sendSuccess, sendError } = useResponseSchemas(
    SuccessSchema,
    ErrorSchema
);

useRoute("contacts")
    .post("/")
    .version('1')
    .codes(codes) // Registers only 200, 400, 401, 403
    .handler(async (req) => {
        try {
            const contact = await contactService.create(req.body);
            return sendSuccess(contact);
        } catch (error) {
            return error instanceof ResponseError ? error : response400("Failed to create contact");
        }
    })
    .build();

2. Using buildExtraResponseCodes (Recommended):
useRoute("contacts")
    .post("/")
    .version('1')
    .codes(buildExtraResponseCodes(ContactSchema, ErrorSchema)) // Registers only 200, 400, 401, 403
    .handler(async (req) => {
        try {
            const contact = await contactService.create(req.body);
            return response200(contact);
        } catch (error) {
            return error instanceof ResponseError ? error : response400("Failed to create contact");
        }
    })
    .build();

3. Service Layer Error Handling:
@Service()
class ContactService {
    async findById(id: string) {
        const contact = await this.prisma.contact.findUnique({ where: { id } });
        if (!contact) {
            throw new ResponseBadRequest(`Contact with ID ${id} not found`);
        }
        return contact;
    }
}

4. Include Error Details:
const ValidationErrorDetails = Type.Object({
    field: Type.String(),
    message: Type.String()
});

const ErrorDetailsSchema = Type.Object({
    errors: Type.Array(ValidationErrorDetails)
});

const { register: errorRegister, send: sendError } = useResponseErrorSchema(400, ErrorDetailsSchema);
useRoute("contacts")
    .post("/")
    .code(...errorRegister)
    .handler(async (req) => {
        try {
            const contact = await contactService.create(req.body);
            return response200(contact);
        } catch (error) {
            if (error instanceof ResponseError) {
                return error;
            }
            return sendError(`Invalid input`, {
                errors: [{
                    field: "email",
                    message: "Invalid email format"
                }]
            });
        }
    });

5. Handle Specific Error Types:
try {
    const session = useSession<UserSession>(req);
    const contact = await service.setMainContact(session?.id, req.params.id);
    return { status: 200, data: contact };
} catch (error) {
    if (error instanceof DatabaseError) {
        return { status: 500, data: { error: error.message } };
    }
    if (error instanceof ValidationError) {
        return { status: 400, data: { error: error.message } };
    }
    if (error instanceof UnauthorizedError) {
        return { status: 401, data: { error: error.message } };
    }
    if (error instanceof ForbiddenError) {
        return { status: 403, data: { error: error.message } };
    }
    return error instanceof ResponseError ? error : response400("Operation failed");
}

6. Handle Async Errors:
try {
    await someAsyncOperation();
} catch (error) {
    return error instanceof ResponseError ? error : response400("Operation failed");
}
*/

/* 6. SERVICE LAYER BEST PRACTICES
- Always wrap database operations in try-catch blocks
- Throw appropriate custom errors
- Handle relationships carefully
- Use TypeDI for dependency injection
- Keep business logic in service layer
- Use try-catch blocks for database operations
*/

/* 7. CONTROLLER NAMING AND VERSIONING
- use .controller(<controllerName>) for controller name
- Always specify version for each route
- Use semantic versioning (e.g., "1", "2")
- When making breaking changes, increment major version
- Support multiple versions simultaneously
- Each route must be defined separately with single HTTP method
*/

/* 8. FEATURE STRUCTURE
A feature must contain at least:
- name.service.ts (business logic)
- name.module.ts (route definitions)
*/

/* 9. TYPEBOX SCHEMA GENERATION
- Use Output schemas for responses
- Use Input schemas for request bodies
- Import from @generated/typebox-schemas/models
*/
```
