@yoms/create-monorepo 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -0
  3. package/dist/index.js +649 -0
  4. package/package.json +74 -0
  5. package/templates/backend-hono/base/.env.example +7 -0
  6. package/templates/backend-hono/base/Dockerfile +55 -0
  7. package/templates/backend-hono/base/package.json +30 -0
  8. package/templates/backend-hono/base/src/config/env.ts +12 -0
  9. package/templates/backend-hono/base/src/config/logger.ts +52 -0
  10. package/templates/backend-hono/base/src/index.ts +49 -0
  11. package/templates/backend-hono/base/src/lib/__tests__/response.test.ts +66 -0
  12. package/templates/backend-hono/base/src/lib/errors.ts +87 -0
  13. package/templates/backend-hono/base/src/lib/response.ts +72 -0
  14. package/templates/backend-hono/base/src/middleware/__tests__/error.test.ts +86 -0
  15. package/templates/backend-hono/base/src/middleware/cors.middleware.ts +9 -0
  16. package/templates/backend-hono/base/src/middleware/error.middleware.ts +43 -0
  17. package/templates/backend-hono/base/src/middleware/logger.middleware.ts +14 -0
  18. package/templates/backend-hono/base/src/middleware/rate-limit.middleware.ts +135 -0
  19. package/templates/backend-hono/base/src/routes/__tests__/health.test.ts +36 -0
  20. package/templates/backend-hono/base/src/routes/health.route.ts +14 -0
  21. package/templates/backend-hono/base/src/types/app.types.ts +26 -0
  22. package/templates/backend-hono/base/tsconfig.json +10 -0
  23. package/templates/backend-hono/base/vitest.config.ts +19 -0
  24. package/templates/backend-hono/features/mongodb-prisma/env-additions.txt +2 -0
  25. package/templates/backend-hono/features/mongodb-prisma/package-additions.json +14 -0
  26. package/templates/backend-hono/features/mongodb-prisma/prisma/schema.prisma +18 -0
  27. package/templates/backend-hono/features/mongodb-prisma/src/config/database.ts +43 -0
  28. package/templates/backend-hono/features/mongodb-prisma/src/routes/users.route.ts +82 -0
  29. package/templates/backend-hono/features/mongodb-prisma/src/services/user.service.ts +121 -0
  30. package/templates/backend-hono/features/postgres-prisma/env-additions.txt +2 -0
  31. package/templates/backend-hono/features/postgres-prisma/package-additions.json +15 -0
  32. package/templates/backend-hono/features/postgres-prisma/prisma/schema.prisma +18 -0
  33. package/templates/backend-hono/features/postgres-prisma/src/config/database.ts +43 -0
  34. package/templates/backend-hono/features/postgres-prisma/src/routes/users.route.ts +82 -0
  35. package/templates/backend-hono/features/postgres-prisma/src/services/user.service.ts +121 -0
  36. package/templates/backend-hono/features/redis/env-additions.txt +2 -0
  37. package/templates/backend-hono/features/redis/package-additions.json +8 -0
  38. package/templates/backend-hono/features/redis/src/config/redis.ts +32 -0
  39. package/templates/backend-hono/features/redis/src/services/cache.service.ts +107 -0
  40. package/templates/backend-hono/features/smtp/env-additions.txt +7 -0
  41. package/templates/backend-hono/features/smtp/package-additions.json +8 -0
  42. package/templates/backend-hono/features/smtp/src/config/mail.ts +38 -0
  43. package/templates/backend-hono/features/smtp/src/services/email.service.ts +78 -0
  44. package/templates/backend-hono/features/swagger/package-additions.json +6 -0
  45. package/templates/backend-hono/features/swagger/src/lib/openapi.ts +19 -0
  46. package/templates/backend-hono/features/swagger/src/routes/docs.route.ts +18 -0
  47. package/templates/frontend-nextjs/base/.env.example +2 -0
  48. package/templates/frontend-nextjs/base/app/globals.css +59 -0
  49. package/templates/frontend-nextjs/base/app/layout.tsx +22 -0
  50. package/templates/frontend-nextjs/base/app/page.tsx +49 -0
  51. package/templates/frontend-nextjs/base/components.json +18 -0
  52. package/templates/frontend-nextjs/base/lib/api-client.ts +67 -0
  53. package/templates/frontend-nextjs/base/lib/utils.ts +6 -0
  54. package/templates/frontend-nextjs/base/next.config.ts +8 -0
  55. package/templates/frontend-nextjs/base/package.json +33 -0
  56. package/templates/frontend-nextjs/base/postcss.config.mjs +9 -0
  57. package/templates/frontend-nextjs/base/public/.gitkeep +1 -0
  58. package/templates/frontend-nextjs/base/tailwind.config.ts +58 -0
  59. package/templates/frontend-nextjs/base/tsconfig.json +19 -0
  60. package/templates/shared/base/package.json +19 -0
  61. package/templates/shared/base/src/index.ts +5 -0
  62. package/templates/shared/base/src/schemas/user.schema.ts +34 -0
  63. package/templates/shared/base/src/types.ts +46 -0
  64. package/templates/shared/base/tsconfig.json +9 -0
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@yoms/create-monorepo",
3
+ "version": "1.0.2",
4
+ "description": "CLI tool to scaffold monorepo projects from templates",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-monorepo": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "dev": "tsx src/index.ts",
11
+ "build": "tsup src/index.ts --format esm --clean",
12
+ "typecheck": "tsc --noEmit",
13
+ "lint": "eslint src --ext .ts",
14
+ "format": "prettier --write \"src/**/*.ts\"",
15
+ "test:generate": "tsx scripts/test-generate.ts",
16
+ "test:validate": "tsx scripts/validate-templates.ts",
17
+ "changeset": "changeset",
18
+ "version": "changeset version",
19
+ "release": "pnpm build && changeset publish",
20
+ "prepublishOnly": "pnpm build && pnpm typecheck"
21
+ },
22
+ "keywords": [
23
+ "monorepo",
24
+ "template",
25
+ "scaffold",
26
+ "cli",
27
+ "pnpm",
28
+ "workspace",
29
+ "typescript",
30
+ "hono",
31
+ "nextjs",
32
+ "prisma",
33
+ "generator",
34
+ "boilerplate",
35
+ "starter"
36
+ ],
37
+ "author": "",
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/yoms07/monorepo-template.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/yoms07/monorepo-template/issues"
45
+ },
46
+ "homepage": "https://github.com/yoms07/monorepo-template#readme",
47
+ "files": [
48
+ "dist",
49
+ "templates",
50
+ "README.md",
51
+ "LICENSE"
52
+ ],
53
+ "dependencies": {
54
+ "cac": "^6.7.14",
55
+ "chalk": "^5.3.0",
56
+ "enquirer": "^2.4.1",
57
+ "execa": "^9.5.2",
58
+ "fs-extra": "^11.2.0",
59
+ "ora": "^8.1.1",
60
+ "picocolors": "^1.1.1"
61
+ },
62
+ "devDependencies": {
63
+ "@changesets/cli": "^2.27.10",
64
+ "@types/fs-extra": "^11.0.4",
65
+ "@types/node": "^22.10.5",
66
+ "prettier": "^3.4.2",
67
+ "tsup": "^8.3.5",
68
+ "tsx": "^4.19.2",
69
+ "typescript": "^5.7.2"
70
+ },
71
+ "engines": {
72
+ "node": ">=18.0.0"
73
+ }
74
+ }
@@ -0,0 +1,7 @@
1
+ # Server Configuration
2
+ NODE_ENV=development
3
+ PORT=__API_PORT__
4
+ LOG_LEVEL=info
5
+
6
+ # CORS Configuration
7
+ CORS_ORIGIN=http://localhost:__WEB_PORT__
@@ -0,0 +1,55 @@
1
+ FROM node:20-alpine AS base
2
+
3
+ # Install pnpm
4
+ RUN corepack enable && corepack prepare pnpm@latest --activate
5
+
6
+ # Build stage
7
+ FROM base AS build
8
+
9
+ WORKDIR /app
10
+
11
+ # Copy workspace configuration
12
+ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
13
+
14
+ # Copy package.json files for all workspaces
15
+ COPY packages/api/package.json ./packages/api/
16
+ COPY packages/shared/package.json ./packages/shared/
17
+
18
+ # Install dependencies
19
+ RUN pnpm install --frozen-lockfile
20
+
21
+ # Copy source code
22
+ COPY packages/api ./packages/api
23
+ COPY packages/shared ./packages/shared
24
+
25
+ # Build shared package first, then api
26
+ RUN pnpm --filter __PACKAGE_SCOPE__/shared build && \
27
+ pnpm --filter __PACKAGE_SCOPE__/api build
28
+
29
+ # Production stage
30
+ FROM base AS production
31
+
32
+ WORKDIR /app
33
+
34
+ ENV NODE_ENV=production
35
+
36
+ # Copy workspace configuration
37
+ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
38
+ COPY packages/api/package.json ./packages/api/
39
+ COPY packages/shared/package.json ./packages/shared/
40
+
41
+ # Install production dependencies only
42
+ RUN pnpm install --prod --frozen-lockfile
43
+
44
+ # Copy built files from build stage
45
+ COPY --from=build /app/packages/shared/dist ./packages/shared/dist
46
+ COPY --from=build /app/packages/api/dist ./packages/api/dist
47
+
48
+ # Set working directory to api package
49
+ WORKDIR /app/packages/api
50
+
51
+ # Expose port
52
+ EXPOSE __API_PORT__
53
+
54
+ # Start
55
+ CMD ["node", "dist/index.js"]
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "__PACKAGE_SCOPE__/api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "build": "tsup src/index.ts --format esm --clean",
9
+ "start": "node dist/index.js",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "test:coverage": "vitest run --coverage",
13
+ "typecheck": "tsc --noEmit",
14
+ "lint": "eslint src --ext .ts",
15
+ "format": "prettier --write \"src/**/*.ts\""
16
+ },
17
+ "dependencies": {
18
+ "hono": "^4.6.14",
19
+ "zod": "^3.24.1",
20
+ "winston": "^3.17.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^22.10.5",
24
+ "@vitest/coverage-v8": "^2.1.8",
25
+ "tsx": "^4.19.2",
26
+ "tsup": "^8.3.5",
27
+ "typescript": "^5.7.2",
28
+ "vitest": "^2.1.8"
29
+ }
30
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+
3
+ const envSchema = z.object({
4
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
5
+ PORT: z.coerce.number().default(__API_PORT__),
6
+ LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
7
+ CORS_ORIGIN: z.string().default('http://localhost:__WEB_PORT__'),
8
+ });
9
+
10
+ export const env = envSchema.parse(process.env);
11
+
12
+ export type Env = z.infer<typeof envSchema>;
@@ -0,0 +1,52 @@
1
+ import winston from 'winston';
2
+ import { env } from './env.js';
3
+
4
+ const { combine, timestamp, printf, colorize, errors } = winston.format;
5
+
6
+ // Custom format for development
7
+ const devFormat = printf(({ level, message, timestamp, stack, ...metadata }) => {
8
+ let msg = `${timestamp} [${level}]: ${message}`;
9
+
10
+ // Add metadata if present
11
+ const metaStr = Object.keys(metadata).length > 0
12
+ ? `\n${JSON.stringify(metadata, null, 2)}`
13
+ : '';
14
+
15
+ // Add stack trace if error
16
+ const stackStr = stack ? `\n${stack}` : '';
17
+
18
+ return msg + metaStr + stackStr;
19
+ });
20
+
21
+ // Custom format for production (JSON)
22
+ const prodFormat = combine(
23
+ errors({ stack: true }),
24
+ timestamp(),
25
+ winston.format.json()
26
+ );
27
+
28
+ const logger = winston.createLogger({
29
+ level: env.LOG_LEVEL,
30
+ format: env.NODE_ENV === 'production' ? prodFormat : combine(
31
+ colorize(),
32
+ timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
33
+ errors({ stack: true }),
34
+ devFormat
35
+ ),
36
+ transports: [
37
+ new winston.transports.Console(),
38
+ ],
39
+ });
40
+
41
+ // Add file transport in production
42
+ if (env.NODE_ENV === 'production') {
43
+ logger.add(new winston.transports.File({
44
+ filename: 'logs/error.log',
45
+ level: 'error'
46
+ }));
47
+ logger.add(new winston.transports.File({
48
+ filename: 'logs/combined.log'
49
+ }));
50
+ }
51
+
52
+ export { logger };
@@ -0,0 +1,49 @@
1
+ import { Hono } from 'hono';
2
+ import { env } from './config/env.js';
3
+ import { logger } from './config/logger.js';
4
+ import { corsMiddleware } from './middleware/cors.middleware.js';
5
+ import { loggerMiddleware } from './middleware/logger.middleware.js';
6
+ import { errorHandler } from './middleware/error.middleware.js';
7
+ import { health } from './routes/health.route.js';
8
+
9
+ const app = new Hono();
10
+
11
+ // Global middleware
12
+ app.use('*', corsMiddleware);
13
+ app.use('*', loggerMiddleware);
14
+
15
+ // Routes
16
+ app.route('/health', health);
17
+
18
+ // Root endpoint
19
+ app.get('/', (c) => {
20
+ return c.json({
21
+ success: true,
22
+ message: 'Welcome to __PROJECT_NAME__ API',
23
+ version: '0.1.0',
24
+ });
25
+ });
26
+
27
+ // Error handling
28
+ app.onError(errorHandler);
29
+
30
+ // 404 handler
31
+ app.notFound((c) => {
32
+ return c.json({
33
+ success: false,
34
+ error: 'Not found',
35
+ message: `Route ${c.req.method} ${c.req.url} not found`,
36
+ }, 404);
37
+ });
38
+
39
+ // Start server
40
+ const port = env.PORT;
41
+
42
+ logger.info(`Starting server in ${env.NODE_ENV} mode...`);
43
+
44
+ export default {
45
+ port,
46
+ fetch: app.fetch,
47
+ };
48
+
49
+ console.log(`Server running on http://localhost:${port}`);
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Hono } from 'hono';
3
+ import { success, created, noContent, paginated, error } from '../response.js';
4
+
5
+ describe('Response Helpers', () => {
6
+ const app = new Hono();
7
+
8
+ app.get('/success', (c) => success(c, { message: 'OK' }));
9
+ app.post('/created', (c) => created(c, { id: '123' }));
10
+ app.delete('/no-content', (c) => noContent(c));
11
+ app.get('/paginated', (c) =>
12
+ paginated(c, [{ id: 1 }, { id: 2 }], 1, 10, 25)
13
+ );
14
+ app.get('/error', (c) => error(c, 'Something failed', 400, 'BAD_REQUEST'));
15
+
16
+ it('should return success response with 200', async () => {
17
+ const res = await app.request('/success');
18
+ expect(res.status).toBe(200);
19
+
20
+ const json = await res.json();
21
+ expect(json.success).toBe(true);
22
+ expect(json.data).toEqual({ message: 'OK' });
23
+ });
24
+
25
+ it('should return created response with 201', async () => {
26
+ const res = await app.request('/created', { method: 'POST' });
27
+ expect(res.status).toBe(201);
28
+
29
+ const json = await res.json();
30
+ expect(json.success).toBe(true);
31
+ expect(json.data).toEqual({ id: '123' });
32
+ });
33
+
34
+ it('should return no content with 204', async () => {
35
+ const res = await app.request('/no-content', { method: 'DELETE' });
36
+ expect(res.status).toBe(204);
37
+ expect(res.body).toBeNull();
38
+ });
39
+
40
+ it('should return paginated response', async () => {
41
+ const res = await app.request('/paginated');
42
+ expect(res.status).toBe(200);
43
+
44
+ const json = await res.json();
45
+ expect(json.success).toBe(true);
46
+ expect(json.data).toHaveLength(2);
47
+ expect(json.pagination).toEqual({
48
+ page: 1,
49
+ limit: 10,
50
+ total: 25,
51
+ totalPages: 3,
52
+ hasNext: true,
53
+ hasPrev: false,
54
+ });
55
+ });
56
+
57
+ it('should return error response', async () => {
58
+ const res = await app.request('/error');
59
+ expect(res.status).toBe(400);
60
+
61
+ const json = await res.json();
62
+ expect(json.success).toBe(false);
63
+ expect(json.error).toBe('Something failed');
64
+ expect(json.code).toBe('BAD_REQUEST');
65
+ });
66
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Base application error class
3
+ */
4
+ export class AppError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public statusCode: number = 500,
8
+ public code?: string,
9
+ public details?: unknown
10
+ ) {
11
+ super(message);
12
+ this.name = this.constructor.name;
13
+ Error.captureStackTrace(this, this.constructor);
14
+ }
15
+ }
16
+
17
+ /**
18
+ * 400 Bad Request
19
+ */
20
+ export class BadRequestError extends AppError {
21
+ constructor(message: string = 'Bad request', details?: unknown) {
22
+ super(message, 400, 'BAD_REQUEST', details);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * 401 Unauthorized
28
+ */
29
+ export class UnauthorizedError extends AppError {
30
+ constructor(message: string = 'Unauthorized', details?: unknown) {
31
+ super(message, 401, 'UNAUTHORIZED', details);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * 403 Forbidden
37
+ */
38
+ export class ForbiddenError extends AppError {
39
+ constructor(message: string = 'Forbidden', details?: unknown) {
40
+ super(message, 403, 'FORBIDDEN', details);
41
+ }
42
+ }
43
+
44
+ /**
45
+ * 404 Not Found
46
+ */
47
+ export class NotFoundError extends AppError {
48
+ constructor(message: string = 'Resource not found', details?: unknown) {
49
+ super(message, 404, 'NOT_FOUND', details);
50
+ }
51
+ }
52
+
53
+ /**
54
+ * 409 Conflict
55
+ */
56
+ export class ConflictError extends AppError {
57
+ constructor(message: string = 'Resource already exists', details?: unknown) {
58
+ super(message, 409, 'CONFLICT', details);
59
+ }
60
+ }
61
+
62
+ /**
63
+ * 422 Unprocessable Entity
64
+ */
65
+ export class ValidationError extends AppError {
66
+ constructor(message: string = 'Validation failed', details?: unknown) {
67
+ super(message, 422, 'VALIDATION_ERROR', details);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 429 Too Many Requests
73
+ */
74
+ export class TooManyRequestsError extends AppError {
75
+ constructor(message: string = 'Too many requests', details?: unknown) {
76
+ super(message, 429, 'TOO_MANY_REQUESTS', details);
77
+ }
78
+ }
79
+
80
+ /**
81
+ * 500 Internal Server Error
82
+ */
83
+ export class InternalServerError extends AppError {
84
+ constructor(message: string = 'Internal server error', details?: unknown) {
85
+ super(message, 500, 'INTERNAL_SERVER_ERROR', details);
86
+ }
87
+ }
@@ -0,0 +1,72 @@
1
+ import type { Context } from 'hono';
2
+ import type { StatusCode } from 'hono/utils/http-status';
3
+ import type { ApiResponse } from '../types/app.types.js';
4
+
5
+ /**
6
+ * Success response helper
7
+ */
8
+ export function success<T>(c: Context, data: T, statusCode: StatusCode = 200) {
9
+ const response: ApiResponse<T> = {
10
+ success: true,
11
+ data,
12
+ };
13
+ return c.json(response, statusCode);
14
+ }
15
+
16
+ /**
17
+ * Error response helper
18
+ */
19
+ export function error(
20
+ c: Context,
21
+ message: string,
22
+ statusCode: StatusCode = 500,
23
+ code?: string,
24
+ details?: unknown
25
+ ) {
26
+ const response: ApiResponse = {
27
+ success: false,
28
+ error: message,
29
+ code,
30
+ details: process.env.NODE_ENV === 'development' ? details : undefined,
31
+ };
32
+ return c.json(response, statusCode);
33
+ }
34
+
35
+ /**
36
+ * Created response (201)
37
+ */
38
+ export function created<T>(c: Context, data: T) {
39
+ return success(c, data, 201);
40
+ }
41
+
42
+ /**
43
+ * No content response (204)
44
+ */
45
+ export function noContent(c: Context) {
46
+ return c.body(null, 204);
47
+ }
48
+
49
+ /**
50
+ * Paginated response helper
51
+ */
52
+ export function paginated<T>(
53
+ c: Context,
54
+ data: T[],
55
+ page: number,
56
+ limit: number,
57
+ total: number
58
+ ) {
59
+ const response = {
60
+ success: true,
61
+ data,
62
+ pagination: {
63
+ page,
64
+ limit,
65
+ total,
66
+ totalPages: Math.ceil(total / limit),
67
+ hasNext: page * limit < total,
68
+ hasPrev: page > 1,
69
+ },
70
+ };
71
+ return c.json(response);
72
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Hono } from 'hono';
3
+ import { errorHandler } from '../error.middleware.js';
4
+ import {
5
+ BadRequestError,
6
+ NotFoundError,
7
+ UnauthorizedError,
8
+ InternalServerError,
9
+ } from '../../lib/errors.js';
10
+
11
+ describe('Error Middleware', () => {
12
+ const app = new Hono();
13
+
14
+ // Test route that throws errors
15
+ app.get('/bad-request', () => {
16
+ throw new BadRequestError('Invalid input');
17
+ });
18
+
19
+ app.get('/not-found', () => {
20
+ throw new NotFoundError('Resource not found');
21
+ });
22
+
23
+ app.get('/unauthorized', () => {
24
+ throw new UnauthorizedError('Not authenticated');
25
+ });
26
+
27
+ app.get('/server-error', () => {
28
+ throw new InternalServerError('Something went wrong');
29
+ });
30
+
31
+ app.get('/unknown-error', () => {
32
+ throw new Error('Unknown error');
33
+ });
34
+
35
+ app.onError(errorHandler);
36
+
37
+ it('should handle BadRequestError with 400 status', async () => {
38
+ const res = await app.request('/bad-request');
39
+ expect(res.status).toBe(400);
40
+
41
+ const json = await res.json();
42
+ expect(json.success).toBe(false);
43
+ expect(json.error).toBe('Invalid input');
44
+ expect(json.code).toBe('BAD_REQUEST');
45
+ });
46
+
47
+ it('should handle NotFoundError with 404 status', async () => {
48
+ const res = await app.request('/not-found');
49
+ expect(res.status).toBe(404);
50
+
51
+ const json = await res.json();
52
+ expect(json.success).toBe(false);
53
+ expect(json.error).toBe('Resource not found');
54
+ expect(json.code).toBe('NOT_FOUND');
55
+ });
56
+
57
+ it('should handle UnauthorizedError with 401 status', async () => {
58
+ const res = await app.request('/unauthorized');
59
+ expect(res.status).toBe(401);
60
+
61
+ const json = await res.json();
62
+ expect(json.success).toBe(false);
63
+ expect(json.error).toBe('Not authenticated');
64
+ expect(json.code).toBe('UNAUTHORIZED');
65
+ });
66
+
67
+ it('should handle InternalServerError with 500 status', async () => {
68
+ const res = await app.request('/server-error');
69
+ expect(res.status).toBe(500);
70
+
71
+ const json = await res.json();
72
+ expect(json.success).toBe(false);
73
+ expect(json.error).toBe('Something went wrong');
74
+ expect(json.code).toBe('INTERNAL_SERVER_ERROR');
75
+ });
76
+
77
+ it('should handle unknown errors with 500 status', async () => {
78
+ const res = await app.request('/unknown-error');
79
+ expect(res.status).toBe(500);
80
+
81
+ const json = await res.json();
82
+ expect(json.success).toBe(false);
83
+ expect(json.error).toBe('Internal server error');
84
+ expect(json.code).toBe('INTERNAL_SERVER_ERROR');
85
+ });
86
+ });
@@ -0,0 +1,9 @@
1
+ import { cors } from 'hono/cors';
2
+ import { env } from '../config/env.js';
3
+
4
+ export const corsMiddleware = cors({
5
+ origin: env.CORS_ORIGIN.split(',').map(o => o.trim()),
6
+ allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
7
+ allowHeaders: ['Content-Type', 'Authorization'],
8
+ credentials: true,
9
+ });
@@ -0,0 +1,43 @@
1
+ import type { Context } from 'hono';
2
+ import { logger } from '../config/logger.js';
3
+ import { AppError } from '../lib/errors.js';
4
+ import type { ApiResponse } from '../types/app.types.js';
5
+
6
+ export function errorHandler(error: Error, c: Context) {
7
+ // Handle known application errors
8
+ if (error instanceof AppError) {
9
+ logger.warn('Application error:', {
10
+ code: error.code,
11
+ message: error.message,
12
+ statusCode: error.statusCode,
13
+ path: c.req.path,
14
+ method: c.req.method,
15
+ });
16
+
17
+ const response: ApiResponse = {
18
+ success: false,
19
+ error: error.message,
20
+ code: error.code,
21
+ details: process.env.NODE_ENV === 'development' ? error.details : undefined,
22
+ };
23
+
24
+ return c.json(response, error.statusCode);
25
+ }
26
+
27
+ // Handle unknown errors
28
+ logger.error('Unhandled error:', {
29
+ error: error.message,
30
+ stack: error.stack,
31
+ path: c.req.path,
32
+ method: c.req.method,
33
+ });
34
+
35
+ const response: ApiResponse = {
36
+ success: false,
37
+ error: 'Internal server error',
38
+ code: 'INTERNAL_SERVER_ERROR',
39
+ message: process.env.NODE_ENV === 'development' ? error.message : undefined,
40
+ };
41
+
42
+ return c.json(response, 500);
43
+ }
@@ -0,0 +1,14 @@
1
+ import type { Context, Next } from 'hono';
2
+ import { logger } from '../config/logger.js';
3
+
4
+ export async function loggerMiddleware(c: Context, next: Next) {
5
+ const start = Date.now();
6
+ const { method, url } = c.req;
7
+
8
+ await next();
9
+
10
+ const duration = Date.now() - start;
11
+ const status = c.res.status;
12
+
13
+ logger.info(`${method} ${url} ${status} - ${duration}ms`);
14
+ }