feat: enhance API route naming conventions and structure
- Added comprehensive guidelines for API route naming and structure in `ROUTE_NAMING_BEST_PRACTICES.md`. - Updated `ARCHITECHTURE.md` to reference the new best practices document. - Refactored course-related routes to follow new naming conventions, including changes to endpoint paths and validation schemas. - Removed the deprecated course clone route and integrated cloning functionality directly into the course routes. - Adjusted various API methods to ensure consistent usage of HTTP methods and parameter structures across the application.
This commit is contained in:
@@ -0,0 +1,543 @@
|
||||
# API Route Naming and Structure Best Practices
|
||||
|
||||
This guide outlines best practices for naming and structuring API routes, inspired by Stripe's API design principles. These conventions ensure consistency, predictability, and maintainability across all API endpoints.
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. **RESTful Resource Naming**
|
||||
|
||||
Use **plural nouns** for resource names, following REST conventions:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Plural nouns
|
||||
GET /courses
|
||||
POST /courses
|
||||
GET /courses/:id
|
||||
PUT /courses/:id
|
||||
DELETE /courses/:id
|
||||
|
||||
GET /organizations
|
||||
POST /organizations
|
||||
GET /organizations/:id
|
||||
|
||||
// ❌ BAD - Singular nouns
|
||||
GET /course
|
||||
POST /course
|
||||
GET /organization
|
||||
```
|
||||
|
||||
**Why?** Resources represent collections. Even when fetching a single item, you're accessing it from a collection.
|
||||
|
||||
---
|
||||
|
||||
### 2. **URL Structure Hierarchy**
|
||||
|
||||
Organize routes hierarchically to reflect resource relationships:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Clear hierarchy
|
||||
GET /organizations/:orgId/courses
|
||||
POST /organizations/:orgId/courses
|
||||
GET /organizations/:orgId/courses/:courseId
|
||||
PUT /organizations/:orgId/courses/:courseId
|
||||
|
||||
GET /courses/:courseId/lessons
|
||||
POST /courses/:courseId/lessons
|
||||
GET /courses/:courseId/lessons/:lessonId
|
||||
|
||||
// ❌ BAD - Flat structure loses context
|
||||
GET /courses (which org?)
|
||||
GET /lessons (which course?)
|
||||
```
|
||||
|
||||
**Exception:** Top-level resources that don't belong to a parent:
|
||||
|
||||
```typescript
|
||||
// ✅ OK - Top-level resources
|
||||
GET /account
|
||||
PUT /account/user
|
||||
GET /account/subscription
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **HTTP Method Semantics**
|
||||
|
||||
Use HTTP methods correctly for their intended purpose:
|
||||
|
||||
| Method | Purpose | Idempotent | Response Body |
|
||||
|--------|---------|------------|---------------|
|
||||
| `GET` | Retrieve resource(s) | ✅ Yes | Always |
|
||||
| `POST` | Create new resource | ❌ No | Created resource |
|
||||
| `PUT` | Replace entire resource | ✅ Yes | Updated resource |
|
||||
| `PATCH` | Partial update | ✅ Yes | Updated resource |
|
||||
| `DELETE` | Remove resource | ✅ Yes | Usually empty (204) |
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Correct method usage
|
||||
GET /courses/:id // Retrieve course
|
||||
POST /courses // Create course
|
||||
PUT /courses/:id // Replace entire course
|
||||
PATCH /courses/:id // Partial update
|
||||
DELETE /courses/:id // Delete course
|
||||
|
||||
// ❌ BAD - Wrong method
|
||||
POST /courses/:id/update // Should be PUT or PATCH
|
||||
GET /courses/:id/delete // Should be DELETE
|
||||
POST /courses/:id // Should be PUT/PATCH for updates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. **Nested Resources**
|
||||
|
||||
Nest resources when there's a clear parent-child relationship:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Nested resources
|
||||
GET /courses/:courseId/lessons
|
||||
POST /courses/:courseId/lessons
|
||||
GET /courses/:courseId/lessons/:lessonId
|
||||
PUT /courses/:courseId/lessons/:lessonId
|
||||
|
||||
GET /organizations/:orgId/members
|
||||
POST /organizations/:orgId/members
|
||||
DELETE /organizations/:orgId/members/:memberId
|
||||
|
||||
// ❌ BAD - Flat structure with IDs
|
||||
GET /lessons?courseId=123 // Less clear hierarchy
|
||||
POST /lessons?courseId=123 // Relationship not obvious
|
||||
```
|
||||
|
||||
**When to nest:**
|
||||
- ✅ Child resource only exists within parent context
|
||||
- ✅ Child resource ID is unique within parent (e.g., `lessonId` within `courseId`)
|
||||
- ✅ Operations typically scoped to parent
|
||||
|
||||
**When NOT to nest:**
|
||||
- ❌ Resource can exist independently
|
||||
- ❌ Resource ID is globally unique
|
||||
- ❌ Operations span multiple parents
|
||||
|
||||
```typescript
|
||||
// ✅ OK - Independent resource
|
||||
GET /users/:userId // User exists independently
|
||||
GET /courses/:courseId // Course exists independently
|
||||
|
||||
// ✅ BAD - Unnecessary nesting
|
||||
GET /organizations/:orgId/users/:userId // User is global, not org-specific
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. **Action Endpoints (Non-Resource Operations)**
|
||||
|
||||
For operations that don't map to CRUD, use action verbs as sub-resources:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Action endpoints
|
||||
POST /courses/:id/clone
|
||||
POST /courses/:id/publish
|
||||
POST /courses/:id/unpublish
|
||||
POST /organizations/:id/invite
|
||||
POST /subscriptions/:id/cancel
|
||||
POST /subscriptions/:id/reactivate
|
||||
|
||||
// ❌ BAD - Verbs in main path
|
||||
POST /clone-course/:id
|
||||
POST /publish-course/:id
|
||||
GET /get-courses // Redundant verb
|
||||
```
|
||||
|
||||
**Pattern:** `POST /resource/:id/action`
|
||||
|
||||
**When to use:**
|
||||
- ✅ State transitions (publish, unpublish, activate, cancel)
|
||||
- ✅ Side effects (clone, duplicate, export)
|
||||
- ✅ Complex operations that don't fit CRUD
|
||||
|
||||
---
|
||||
|
||||
### 6. **Query Parameters**
|
||||
|
||||
Use query parameters for filtering, sorting, pagination, and optional data:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Query parameters
|
||||
GET /courses?status=published
|
||||
GET /courses?orgId=123&status=published
|
||||
GET /courses?page=1&limit=20
|
||||
GET /courses?sort=created_at&order=desc
|
||||
GET /courses?search=javascript
|
||||
GET /organizations?siteName=acme
|
||||
|
||||
// ❌ BAD - Path parameters for filters
|
||||
GET /courses/status/published
|
||||
GET /courses/page/1
|
||||
GET /courses/search/javascript
|
||||
```
|
||||
|
||||
**Common query parameter patterns:**
|
||||
|
||||
| Purpose | Parameter | Example |
|
||||
|---------|-----------|---------|
|
||||
| Filtering | `status`, `type`, `role` | `?status=active&type=course` |
|
||||
| Search | `q`, `search` | `?q=javascript` |
|
||||
| Pagination | `page`, `limit`, `offset` | `?page=1&limit=20` |
|
||||
| Sorting | `sort`, `order` | `?sort=created_at&order=desc` |
|
||||
| Date range | `startDate`, `endDate` | `?startDate=2024-01-01` |
|
||||
| Boolean flags | `include`, `expand` | `?include=lessons&expand=metadata` |
|
||||
|
||||
---
|
||||
|
||||
### 7. **URL Parameter Naming**
|
||||
|
||||
Use **camelCase** for URL parameters to match JavaScript conventions:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - camelCase
|
||||
GET /courses/:courseId
|
||||
GET /organizations/:orgId
|
||||
GET /users/:userId
|
||||
GET /lessons/:lessonId
|
||||
|
||||
// ❌ BAD - snake_case or kebab-case
|
||||
GET /courses/:course_id
|
||||
GET /courses/:course-id
|
||||
```
|
||||
|
||||
**Exception:** When matching database column names exactly:
|
||||
|
||||
```typescript
|
||||
// ✅ OK - Matching database convention
|
||||
GET /courses/:id // Generic ID
|
||||
GET /courses?siteName=acme // Matches DB column
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. **Consistent Response Structure**
|
||||
|
||||
Maintain consistent response formats across all endpoints:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Consistent structure
|
||||
// Single resource
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... }
|
||||
}
|
||||
|
||||
// Collection
|
||||
{
|
||||
"success": true,
|
||||
"data": [ ... ],
|
||||
"pagination": {
|
||||
"page": 1,
|
||||
"limit": 20,
|
||||
"total": 100,
|
||||
"totalPages": 5
|
||||
}
|
||||
}
|
||||
|
||||
// Error
|
||||
{
|
||||
"success": false,
|
||||
"error": "Human-readable message",
|
||||
"code": "ERROR_CODE",
|
||||
"field": "fieldName" // Optional, for validation errors
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. **Versioning Strategy**
|
||||
|
||||
Plan for API versioning from the start:
|
||||
|
||||
```typescript
|
||||
// Current (v1 implied)
|
||||
.route('/courses', courseRouter)
|
||||
|
||||
// Future versioning
|
||||
.route('/v1/courses', courseRouterV1)
|
||||
.route('/v2/courses', courseRouterV2)
|
||||
|
||||
// Or use headers (Stripe style)
|
||||
// X-API-Version: 2024-01-01
|
||||
```
|
||||
|
||||
**Recommendation:** Start without versioning, add `/v1/` prefix when breaking changes are needed.
|
||||
|
||||
---
|
||||
|
||||
### 10. **Route Organization in Code**
|
||||
|
||||
Organize routes by domain/resource:
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD - Organized by resource
|
||||
apps/api/src/routes/
|
||||
├── course/
|
||||
│ ├── course.ts // Main course routes
|
||||
│ ├── lesson.ts // Lesson routes
|
||||
│ ├── clone.ts // Clone action
|
||||
│ └── index.ts // Re-exports
|
||||
├── organization/
|
||||
│ ├── organization.ts // Main org routes
|
||||
│ ├── team.ts // Team member routes
|
||||
│ └── index.ts
|
||||
└── account/
|
||||
├── account.ts // Account routes
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
**Route registration:**
|
||||
|
||||
```typescript
|
||||
// apps/api/src/app.ts
|
||||
import { courseRouter } from '@api/routes/course';
|
||||
import { organizationRouter } from '@api/routes/organization';
|
||||
import { accountRouter } from '@api/routes/account';
|
||||
|
||||
export const app = new Hono()
|
||||
.route('/course', courseRouter) // /course/*
|
||||
.route('/organization', organizationRouter) // /organization/*
|
||||
.route('/account', accountRouter); // /account/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Examples
|
||||
|
||||
### Example 1: Course Management
|
||||
|
||||
```typescript
|
||||
// apps/api/src/routes/course/course.ts
|
||||
export const courseRouter = new Hono()
|
||||
// List courses (with filters)
|
||||
.get('/', authMiddleware, zValidator('query', ZListCourses), async (c) => {
|
||||
// GET /course?orgId=123&status=published&page=1&limit=20
|
||||
})
|
||||
|
||||
// Get single course
|
||||
.get('/:courseId', authMiddleware, zValidator('param', ZGetCourse), async (c) => {
|
||||
// GET /course/:courseId
|
||||
})
|
||||
|
||||
// Create course
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateCourse), async (c) => {
|
||||
// POST /course
|
||||
})
|
||||
|
||||
// Update course (full)
|
||||
.put('/:courseId', authMiddleware, zValidator('json', ZUpdateCourse), async (c) => {
|
||||
// PUT /course/:courseId
|
||||
})
|
||||
|
||||
// Update course (partial)
|
||||
.patch('/:courseId', authMiddleware, zValidator('json', ZPatchCourse), async (c) => {
|
||||
// PATCH /course/:courseId
|
||||
})
|
||||
|
||||
// Delete course
|
||||
.delete('/:courseId', authMiddleware, zValidator('param', ZDeleteCourse), async (c) => {
|
||||
// DELETE /course/:courseId
|
||||
})
|
||||
|
||||
// Actions
|
||||
.post('/:courseId/clone', authMiddleware, zValidator('json', ZCloneCourse), async (c) => {
|
||||
// POST /course/:courseId/clone
|
||||
})
|
||||
|
||||
.post('/:courseId/publish', authMiddleware, zValidator('param', ZPublishCourse), async (c) => {
|
||||
// POST /course/:courseId/publish
|
||||
});
|
||||
```
|
||||
|
||||
### Example 2: Nested Resources
|
||||
|
||||
```typescript
|
||||
// apps/api/src/routes/course/lesson.ts
|
||||
export const lessonRouter = new Hono()
|
||||
// List lessons for a course
|
||||
.get('/', authMiddleware, zValidator('query', ZListLessons), async (c) => {
|
||||
// GET /course/:courseId/lesson?page=1&limit=20
|
||||
})
|
||||
|
||||
// Get single lesson
|
||||
.get('/:lessonId', authMiddleware, zValidator('param', ZGetLesson), async (c) => {
|
||||
// GET /course/:courseId/lesson/:lessonId
|
||||
})
|
||||
|
||||
// Create lesson
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateLesson), async (c) => {
|
||||
// POST /course/:courseId/lesson
|
||||
})
|
||||
|
||||
// Update lesson
|
||||
.put('/:lessonId', authMiddleware, zValidator('json', ZUpdateLesson), async (c) => {
|
||||
// PUT /course/:courseId/lesson/:lessonId
|
||||
})
|
||||
|
||||
// Delete lesson
|
||||
.delete('/:lessonId', authMiddleware, zValidator('param', ZDeleteLesson), async (c) => {
|
||||
// DELETE /course/:courseId/lesson/:lessonId
|
||||
});
|
||||
|
||||
// Register nested route
|
||||
// apps/api/src/routes/course/course.ts
|
||||
export const courseRouter = new Hono()
|
||||
// ... course routes ...
|
||||
.route('/:courseId/lesson', lessonRouter); // Nested under course
|
||||
```
|
||||
|
||||
### Example 3: Organization with Team
|
||||
|
||||
```typescript
|
||||
// apps/api/src/routes/organization/organization.ts
|
||||
export const organizationRouter = new Hono()
|
||||
// List organizations
|
||||
.get('/', authMiddleware, zValidator('query', ZListOrgs), async (c) => {
|
||||
// GET /organization?siteName=acme
|
||||
})
|
||||
|
||||
// Get organization
|
||||
.get('/:orgId', authMiddleware, zValidator('param', ZGetOrg), async (c) => {
|
||||
// GET /organization/:orgId
|
||||
})
|
||||
|
||||
// Create organization
|
||||
.post('/', authMiddleware, zValidator('json', ZCreateOrg), async (c) => {
|
||||
// POST /organization
|
||||
})
|
||||
|
||||
// Update organization
|
||||
.put('/:orgId', authMiddleware, zValidator('json', ZUpdateOrg), async (c) => {
|
||||
// PUT /organization/:orgId
|
||||
})
|
||||
|
||||
// Team management (nested)
|
||||
.get('/:orgId/team', authMiddleware, zValidator('param', ZGetTeam), async (c) => {
|
||||
// GET /organization/:orgId/team
|
||||
})
|
||||
|
||||
.post('/:orgId/team/invite', authMiddleware, zValidator('json', ZInviteTeam), async (c) => {
|
||||
// POST /organization/:orgId/team/invite
|
||||
})
|
||||
|
||||
.delete('/:orgId/team/:memberId', authMiddleware, zValidator('param', ZRemoveMember), async (c) => {
|
||||
// DELETE /organization/:orgId/team/:memberId
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns to Avoid
|
||||
|
||||
### ❌ Don't: Use verbs in resource names
|
||||
|
||||
```typescript
|
||||
// ❌ BAD
|
||||
GET /get-courses
|
||||
POST /create-course
|
||||
POST /update-course/:id
|
||||
GET /delete-course/:id
|
||||
|
||||
// ✅ GOOD
|
||||
GET /courses
|
||||
POST /courses
|
||||
PUT /courses/:id
|
||||
DELETE /courses/:id
|
||||
```
|
||||
|
||||
### ❌ Don't: Mix singular and plural
|
||||
|
||||
```typescript
|
||||
// ❌ BAD
|
||||
GET /course
|
||||
POST /courses
|
||||
GET /organization
|
||||
POST /organizations
|
||||
|
||||
// ✅ GOOD
|
||||
GET /courses
|
||||
POST /courses
|
||||
GET /organizations
|
||||
POST /organizations
|
||||
```
|
||||
|
||||
### ❌ Don't: Use query parameters for required identifiers
|
||||
|
||||
```typescript
|
||||
// ❌ BAD
|
||||
GET /courses?id=123
|
||||
GET /courses?courseId=123
|
||||
|
||||
// ✅ GOOD
|
||||
GET /courses/:courseId
|
||||
GET /courses/123
|
||||
```
|
||||
|
||||
### ❌ Don't: Create deep nesting (>3 levels)
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Too deep
|
||||
GET /organizations/:orgId/courses/:courseId/lessons/:lessonId/exercises/:exerciseId
|
||||
|
||||
// ✅ GOOD - Flatten when appropriate
|
||||
GET /exercises/:exerciseId?courseId=123&lessonId=456
|
||||
// Or use a different resource structure
|
||||
GET /lessons/:lessonId/exercises/:exerciseId
|
||||
```
|
||||
|
||||
### ❌ Don't: Use inconsistent naming
|
||||
|
||||
```typescript
|
||||
// ❌ BAD - Mixed conventions
|
||||
GET /courses/:course_id
|
||||
GET /organizations/:orgId
|
||||
GET /users/:userId
|
||||
|
||||
// ✅ GOOD - Consistent camelCase
|
||||
GET /courses/:courseId
|
||||
GET /organizations/:orgId
|
||||
GET /users/:userId
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist for New Routes
|
||||
|
||||
When creating a new route, ensure:
|
||||
|
||||
- [ ] Resource name is **plural** (e.g., `/courses`, not `/course`)
|
||||
- [ ] URL parameters use **camelCase** (e.g., `:courseId`, not `:course_id`)
|
||||
- [ ] HTTP method matches operation (GET=read, POST=create, PUT=replace, PATCH=update, DELETE=remove)
|
||||
- [ ] Nested resources reflect parent-child relationships
|
||||
- [ ] Action endpoints use `POST /resource/:id/action` pattern
|
||||
- [ ] Query parameters used for filtering, pagination, sorting
|
||||
- [ ] Route is registered in main `app.ts`
|
||||
- [ ] Validation schema exists in `packages/utils/src/validation/`
|
||||
- [ ] Route follows existing patterns in codebase
|
||||
- [ ] Response format is consistent with other routes
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Key Takeaways:**
|
||||
|
||||
1. **Plural nouns** for resources (`/courses`, `/organizations`)
|
||||
2. **camelCase** for parameters (`:courseId`, `:orgId`)
|
||||
3. **Hierarchical nesting** for related resources
|
||||
4. **HTTP methods** used semantically (GET, POST, PUT, PATCH, DELETE)
|
||||
5. **Action verbs** as sub-resources for non-CRUD operations
|
||||
6. **Query parameters** for filtering, pagination, sorting
|
||||
7. **Consistent structure** across all endpoints
|
||||
8. **Organized by domain** in code structure
|
||||
|
||||
Following these conventions ensures your API is **predictable**, **maintainable**, and **developer-friendly**.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import * as z from 'zod';
|
||||
|
||||
const envSchema = z.object({
|
||||
CLOUDFLARE_ACCESS_KEY: z.string().optional(),
|
||||
|
||||
@@ -28,7 +28,7 @@ export const accountRouter = new Hono()
|
||||
return handleError(c, error, 'Failed to fetch account data');
|
||||
}
|
||||
})
|
||||
.put('/user', authMiddleware, zValidator('json', ZUpdateProfile), async (c) => {
|
||||
.put('/profile', authMiddleware, zValidator('json', ZUpdateProfile), async (c) => {
|
||||
const user = c.get('user');
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import type { CloneCourseErrorResponse, CloneCourseResponse } from '@api/types/database';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { ZCourseClone } from '@cio/utils/validation/course';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { cloneCourse } from '@api/services/course/clone';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const cloneRouter = new Hono().post('/', authMiddleware, zValidator('json', ZCourseClone), async (c) => {
|
||||
try {
|
||||
const validatedData = c.req.valid('json');
|
||||
const { id, title, description, slug, organizationId } = validatedData;
|
||||
|
||||
const user = c.get('user');
|
||||
|
||||
// Clone the course
|
||||
const newCourse = await cloneCourse(id, title, user.id, description, slug, organizationId);
|
||||
|
||||
const successResponse: CloneCourseResponse = {
|
||||
success: true,
|
||||
course: newCourse
|
||||
};
|
||||
return c.json(successResponse, 200);
|
||||
} catch (error) {
|
||||
console.error('Error cloning course:', error);
|
||||
const errorResponse: CloneCourseErrorResponse = {
|
||||
error: 'Failed to clone course',
|
||||
details: error instanceof Error ? error.message : 'Unknown error'
|
||||
};
|
||||
return c.json(errorResponse, 500);
|
||||
}
|
||||
});
|
||||
@@ -1,49 +1,94 @@
|
||||
import { ZCertificateDownload, ZCourseDownloadContent } from '@cio/utils/validation/course';
|
||||
import {
|
||||
ZCertificateDownload,
|
||||
ZCourseClone,
|
||||
ZCourseCloneParam,
|
||||
ZCourseDownloadContent,
|
||||
ZCourseDownloadParam
|
||||
} from '@cio/utils/validation/course';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import { cloneRouter } from '@api/routes/course/clone';
|
||||
import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { cloneCourse } from '@api/services/course/clone';
|
||||
import { generateCertificate } from '@api/utils/certificate';
|
||||
import { generateCoursePdf } from '@api/utils/course';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { katexRouter } from '@api/routes/course/katex';
|
||||
import { lessonRouter } from '@api/routes/course/lesson';
|
||||
import { presignRouter } from '@api/routes/course/presign';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const courseRouter = new Hono()
|
||||
.post('/download/certificate', zValidator('json', ZCertificateDownload), async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
.post(
|
||||
'/:courseId/download/certificate',
|
||||
zValidator('param', ZCourseDownloadParam),
|
||||
zValidator('json', ZCertificateDownload),
|
||||
async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const result = await generateCertificate(validatedData);
|
||||
const result = await generateCertificate(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header('Content-Disposition', `attachment; filename="certificate-${validatedData.courseName}.pdf"`);
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header('Content-Disposition', `attachment; filename="certificate-${validatedData.courseName}.pdf"`);
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(result);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
.post('/download/content', zValidator('json', ZCourseDownloadContent), async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(result);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/:courseId/download/content',
|
||||
zValidator('param', ZCourseDownloadParam),
|
||||
zValidator('json', ZCourseDownloadContent),
|
||||
async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
const pdfBuffer = await generateCoursePdf(validatedData);
|
||||
const pdfBuffer = await generateCoursePdf(validatedData);
|
||||
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
c.header('Content-Type', 'application/pdf');
|
||||
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
return c.body(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(pdfBuffer);
|
||||
controller.close();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
)
|
||||
.post(
|
||||
'/:courseId/clone',
|
||||
authMiddleware,
|
||||
zValidator('param', ZCourseCloneParam),
|
||||
zValidator('json', ZCourseClone),
|
||||
async (c) => {
|
||||
try {
|
||||
const { courseId } = c.req.valid('param');
|
||||
const validatedData = c.req.valid('json');
|
||||
const { title, description, slug, organizationId } = validatedData;
|
||||
|
||||
const user = c.get('user');
|
||||
|
||||
// Clone the course
|
||||
const newCourse = await cloneCourse(courseId, title, user.id, description, slug, organizationId);
|
||||
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
course: newCourse
|
||||
},
|
||||
200
|
||||
);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to clone course');
|
||||
}
|
||||
}
|
||||
)
|
||||
.route('/katex', katexRouter)
|
||||
.route('/lesson', lessonRouter)
|
||||
.route('/presign', presignRouter)
|
||||
.route('/clone', cloneRouter);
|
||||
.route('/presign', presignRouter);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import * as z from 'zod';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
import katex from 'katex';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const katexRouter = new Hono().get('/', async (c) => {
|
||||
try {
|
||||
|
||||
@@ -5,17 +5,17 @@ import { getOrganisationAnalytics } from '@api/services/dash';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const dashAnalyticsRouter = new Hono().post(
|
||||
export const dashAnalyticsRouter = new Hono().get(
|
||||
'/stats',
|
||||
authMiddleware,
|
||||
zValidator('json', ZDashStats),
|
||||
zValidator('query', ZDashStats),
|
||||
async (c) => {
|
||||
try {
|
||||
const { orgId, siteName } = c.req.valid('json');
|
||||
const { orgId, siteName } = c.req.valid('query');
|
||||
|
||||
const result = await getOrganisationAnalytics(orgId, siteName);
|
||||
|
||||
return c.json({ success: true, data: result }, 201);
|
||||
return c.json({ success: true, data: result }, 200);
|
||||
} catch (error) {
|
||||
return handleError(c, error, 'Failed to load organisation analytics');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { deliverEmail } from '@cio/email';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const mailRouter = new Hono().post('/send', zValidator('json', ZDeliverEmail), async (c) => {
|
||||
export const mailRouter = new Hono().post('/', zValidator('json', ZDeliverEmail), async (c) => {
|
||||
const validatedData = c.req.valid('json');
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { authMiddleware } from '@api/middlewares/auth';
|
||||
import { handleError } from '@api/utils/errors';
|
||||
import { uploadImage } from '@api/services/media';
|
||||
|
||||
export const mediaRouter = new Hono().post('/image/upload', authMiddleware, async (c) => {
|
||||
export const mediaRouter = new Hono().post('/image', authMiddleware, async (c) => {
|
||||
try {
|
||||
const body = await c.req.parseBody();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ZOnboardingStep1, ZOnboardingStep2 } from '@cio/utils/validation/onboarding';
|
||||
import { ZOnboardingCreateOrg, ZOnboardingUpdateMetadata } from '@cio/utils/validation/onboarding';
|
||||
import { completeOnboarding, createOrganizationWithOwner, updateUserOnboarding } from '@api/services/onboarding';
|
||||
|
||||
import { Hono } from '@api/utils/hono';
|
||||
@@ -7,7 +7,7 @@ import { handleError } from '@api/utils/errors';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
|
||||
export const onboardingRouter = new Hono()
|
||||
.post('/step1', authMiddleware, zValidator('json', ZOnboardingStep1), async (c) => {
|
||||
.post('/create-org', authMiddleware, zValidator('json', ZOnboardingCreateOrg), async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
const { fullname, orgName, siteName } = c.req.valid('json');
|
||||
@@ -19,7 +19,7 @@ export const onboardingRouter = new Hono()
|
||||
return handleError(c, error, 'Failed to create organization');
|
||||
}
|
||||
})
|
||||
.post('/step2', authMiddleware, zValidator('json', ZOnboardingStep2), async (c) => {
|
||||
.post('/update-metadata', authMiddleware, zValidator('json', ZOnboardingUpdateMetadata), async (c) => {
|
||||
try {
|
||||
const user = c.get('user');
|
||||
const { goal, source, fullname } = c.req.valid('json');
|
||||
|
||||
Reference in New Issue
Block a user