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:
Rotimi Best
2025-12-05 17:24:16 +01:00
parent eff364baa4
commit 4b52906445
34 changed files with 777 additions and 189 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
import { z } from 'zod';
import * as z from 'zod';
const envSchema = z.object({
CLOUDFLARE_ACCESS_KEY: z.string().optional(),
+1 -1
View File
@@ -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 {
-32
View File
@@ -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);
}
});
+76 -31
View File
@@ -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);
+2 -2
View File
@@ -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 {
+4 -4
View File
@@ -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');
}
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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();
+3 -3
View File
@@ -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');