2025-11-24 01:00:50 +01:00
# API Route Development Guide
## Overview
2026-03-09 11:18:22 +01:00
This guide outlines standard practices for creating API routes in ClassroomIO. **All routes should be designed as potential public API endpoints.**
2025-11-24 01:00:50 +01:00
---
## Architecture Layers
```txt
┌─────────────────────────────────────┐
│ Frontend (Svelte/SvelteKit) │
│ - UI Components │
│ - API Client (RPC with Hono) │
│ - Client-side Validation │
└──────────────┬──────────────────────┘
│ ┌────────────────────────────┐
2026-03-09 11:18:22 +01:00
├──┤ Shared Validation Layer │
│ │ (@cio/utils/validation) │
2025-11-24 01:00:50 +01:00
│ └────────────────────────────┘
▼
┌─────────────────────────────────────┐
│ Route Layer (Hono) │
│ - HTTP Request/Response │
│ - Input Validation (Zod) │
│ - Authentication Check │
└──────────────┬──────────────────────┘
▼
┌─────────────────────────────────────┐
│ Service Layer │
│ - Business Logic │
│ - Transaction Orchestration │
│ - Error Handling │
└──────────────┬──────────────────────┘
▼
┌─────────────────────────────────────┐
│ Query Layer (Drizzle ORM) │
│ - Database Operations │
│ - Pure Functions │
└─────────────────────────────────────┘
```
**Key Principles** :
- **Shared Validation**: Zod schemas in `@cio/utils/validation` used by both API and frontend
- **Routes**: Handle HTTP concerns only (validation, auth, serialization)
- **Services**: Contain all business logic and orchestrate queries
2026-03-09 11:18:22 +01:00
- **Services Utils**: Pure helper logic lives in `apps/api/src/services/{domain}/utils.ts`
2025-11-24 01:00:50 +01:00
- **Queries**: Pure database operations with no business logic
---
## Type Generation
**Location** : `packages/db/src/types.ts` (auto-generated)
```typescript
// packages/db/src/types.ts (auto-generated - DO NOT EDIT)
2025-12-02 15:27:41 +01:00
export type TUser = typeof schema . user . $inferSelect ; // For reading
export type TNewUser = typeof schema . user . $inferInsert ; // For creating
2025-11-24 01:00:50 +01:00
```
2026-03-09 11:18:22 +01:00
**Regenerate after schema changes:**
2025-11-24 01:00:50 +01:00
```bash
2026-03-09 11:18:22 +01:00
cd packages/db && pnpm generate-types
2025-11-24 01:00:50 +01:00
```
---
2026-03-09 11:18:22 +01:00
## Layer 1: Queries
2025-11-24 01:00:50 +01:00
**Location** : `packages/db/src/queries/{domain}/{entity}.ts`
```typescript
import * as schema from '@db/schema' ;
import { db } from '@db/drizzle' ;
import { eq } from 'drizzle-orm' ;
import type { TNewOrganization , TOrganization } from '@db/types' ;
export const createOrganization = async ( data : TNewOrganization ) => {
2025-12-02 15:27:41 +01:00
const [ organization ] = await db . insert ( schema . organization ). values ( data ). returning ();
2025-11-24 01:00:50 +01:00
return organization ;
};
2026-03-09 11:18:22 +01:00
export const updateOrganization = async ( id : string , data : Partial < TOrganization >) => {
2025-11-24 01:00:50 +01:00
const [ organization ] = await db
. update ( schema . organization )
. set ( data )
. where ( eq ( schema . organization . id , id ))
. returning ();
return organization ;
};
```
2026-03-09 11:18:22 +01:00
**Best Practices:**
- Use `TNew{Table}` for creates, `Partial<T{Table}>` for updates
- Use `.returning()` for insert/update operations
- Keep queries pure (no business logic, no HTTP calls, no transactions)
2025-11-24 01:00:50 +01:00
---
2026-03-09 11:18:22 +01:00
## Layer 2: Services
2025-11-24 01:00:50 +01:00
**Location** : `apps/api/src/services/{domain}.ts`
```typescript
import { db } from '@cio/db/drizzle' ;
2026-03-09 11:18:22 +01:00
import { createOrganization , createOrganizationMember , checkSiteNameExists } from '@cio/db/queries/organization' ;
2025-11-24 01:00:50 +01:00
import { AppError , ErrorCodes } from '@api/utils/errors' ;
2026-03-09 11:18:22 +01:00
export async function createOrganizationWithOwner ( profileId : string , input : { orgName : string ; siteName : string }) {
2025-11-24 01:00:50 +01:00
const exists = await checkSiteNameExists ( input . siteName );
if ( exists ) {
2026-03-09 11:18:22 +01:00
throw new AppError ( `Site name already exists` , ErrorCodes . SITENAME_EXISTS , 409 , 'siteName' );
2025-11-24 01:00:50 +01:00
}
2026-03-09 11:18:22 +01:00
const result = await db . transaction ( async ( tx ) => {
const organization = await createOrganization ({ name : input.orgName , siteName : input.siteName });
const member = await createOrganizationMember ({ organizationId : organization.id , profileId , roleId : 1 });
return { organization , member };
});
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
return result ;
2025-11-24 01:00:50 +01:00
}
```
2026-03-09 11:18:22 +01:00
### Error Handling
2025-11-24 01:00:50 +01:00
```typescript
// apps/api/src/utils/errors.ts
export class AppError extends Error {
constructor (
message : string ,
public code : string ,
public statusCode : ContentfulStatusCode = 500 ,
2026-03-09 11:18:22 +01:00
public field? : string
2025-11-24 01:00:50 +01:00
) {
super ( message );
}
}
2026-03-09 11:18:22 +01:00
export const handleError = ( c : Context , error : unknown , fallbackMessage = 'An unexpected error occurred' ) => {
2025-11-24 01:00:50 +01:00
if ( error instanceof AppError ) {
2026-03-09 11:18:22 +01:00
return c . json ({ success : false , error : error.message , code : error.code , field : error.field }, error . statusCode );
2025-11-24 01:00:50 +01:00
}
2026-03-09 11:18:22 +01:00
return c . json ({ success : false , error : fallbackMessage }, 500 );
2025-11-24 01:00:50 +01:00
};
```
2025-12-22 19:57:27 +01:00
**Best Practices:**
2026-03-09 11:18:22 +01:00
- Throw `AppError` with error codes and status codes
- Handle transactions within services
- Return structured data (not HTTP responses)
- Keep services focused on a specific domain
2025-12-22 19:57:27 +01:00
2026-03-09 11:18:22 +01:00
---
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
## Layer 3: Routes
**Location** : `apps/api/src/routes/{domain}/{entity}.ts`
> See [`apps/api/ROUTE_NAMING_BEST_PRACTICES.md`](apps/api/ROUTE_NAMING_BEST_PRACTICES.md) for naming conventions.
2025-11-24 01:00:50 +01:00
```typescript
import { Hono } from '@api/utils/hono' ;
import { authMiddleware } from '@api/middlewares/auth' ;
import { zValidator } from '@hono/zod-validator' ;
import { createOrganizationWithOwner } from '@api/services/organization' ;
import { handleError } from '@api/utils/errors' ;
import { ZCreateOrg } from '@cio/utils/validation/organization' ;
2025-12-02 15:27:41 +01:00
export const organizationRouter = new Hono (). post (
2026-03-09 11:18:22 +01:00
'/' ,
2025-12-02 15:27:41 +01:00
authMiddleware ,
zValidator ( 'json' , ZCreateOrg ),
async ( c ) => {
2025-11-24 01:00:50 +01:00
try {
2026-03-09 11:18:22 +01:00
const user = c . get ( 'user' ) ! ; // Non-null assertion: authMiddleware guarantees user exists
2025-11-24 01:00:50 +01:00
const body = c . req . valid ( 'json' );
const result = await createOrganizationWithOwner ( user . id , body );
return c . json ({ success : true , data : result }, 201 );
} catch ( error ) {
return handleError ( c , error , 'Failed to create organization' );
}
2025-12-02 15:27:41 +01:00
}
);
2025-11-24 01:00:50 +01:00
```
2026-03-09 11:18:22 +01:00
### Response Format
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
```typescript
// Success
{ "success" : true , "data" : { ... } }
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
// Error
{ "success" : false , "error" : "Message" , "code" : "ERROR_CODE" , "field" : "fieldName" }
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
// Paginated
{ "success" : true , "data" : [...], "pagination" : { "page" : 1 , "limit" : 20 , "total" : 100 , "totalPages" : 5 } }
```
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
### HTTP Status Codes
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
| Code | Usage |
|------|-------|
| `200` | GET, PUT, PATCH success |
| `201` | POST creating resource |
| `204` | DELETE success |
| `400` | Validation errors |
| `401` | Missing authentication |
| `403` | Not authorized |
| `404` | Resource not found |
| `409` | Duplicate resource |
| `500` | Server error |
2025-11-24 01:00:50 +01:00
2026-03-09 11:18:22 +01:00
### Authentication Middleware
```typescript
import { authMiddleware } from '@api/middlewares/auth' ;
import { apiKeyMiddleware } from '@api/middlewares/api-key' ;
import { authOrApiKeyMiddleware } from '@api/middlewares/auth-or-api-key' ;
. post ( '/create' , authMiddleware , ...) // User session required
. post ( '/webhook' , apiKeyMiddleware , ...) // API key required
. post ( '/plan' , authOrApiKeyMiddleware , ...) // Either user session or API key
```
**Important** : When using `authMiddleware` , the `user` is guaranteed to be non-null. Use the non-null assertion operator (`!` ) to tell TypeScript this.
### Authorization Middleware
**Generic middleware** (`apps/api/src/middlewares/` ): Reusable across routes
- `orgMemberMiddleware` - User must be org member
- `orgTeamMemberMiddleware` - User must be ADMIN or TUTOR
- `orgAdminMiddleware` - User must be ADMIN
- `courseMemberMiddleware` - User must be course member
**Route-specific middleware** (`apps/api/src/routes/{domain}/middlewares/` ): For resource-specific checks
```typescript
// apps/api/src/routes/community/middlewares/question-author-or-team.ts
export const questionAuthorOrTeamMiddleware = async ( c : Context , next : Next ) => {
const user = c . get ( 'user' ) ! ;
const question = await getQuestionAuthorAndCourse ( c . req . param ( 'id' ));
if ( ! question ) return c . json ({ success : false , error : 'Not found' }, 404 );
const isAuthor = question . authorId === user . id ;
const isTeamMember = await isCourseTeamMemberOrOrgAdmin ( question . courseId , user . id );
if ( ! isAuthor && ! isTeamMember ) return c . json ({ success : false , error : 'Unauthorized' }, 403 );
await next ();
};
```
2025-11-24 01:00:50 +01:00
---
2026-03-09 11:18:22 +01:00
## Validation Schemas
2025-11-24 01:00:50 +01:00
**Location** : `packages/utils/src/validation/{domain}/{entity}.ts`
```typescript
import * as z from 'zod' ;
2025-12-02 09:54:38 +01:00
export const ZCreateOrganization = z . object ({
2026-03-09 11:18:22 +01:00
name : z.string (). min ( 5 ),
siteName : z.string (). min ( 5 )
2025-12-02 09:54:38 +01:00
});
export type TCreateOrganization = z . infer < typeof ZCreateOrganization >;
```
2026-03-09 11:18:22 +01:00
**File Structure:**
2025-12-02 09:54:38 +01:00
```
2026-03-09 11:18:22 +01:00
packages/utils/src/validation/
├── course/course.ts
├── organization/organization.ts
└── index.ts
2025-12-02 09:54:38 +01:00
```