@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
@@ -0,0 +1,135 @@
1
+ import type { Context, Next } from 'hono';
2
+ import { TooManyRequestsError } from '../lib/errors.js';
3
+ import { logger } from '../config/logger.js';
4
+
5
+ interface RateLimitOptions {
6
+ windowMs: number; // Time window in milliseconds
7
+ max: number; // Max requests per window
8
+ message?: string;
9
+ keyGenerator?: (c: Context) => string;
10
+ }
11
+
12
+ interface RateLimitStore {
13
+ get(key: string): Promise<number | null>;
14
+ increment(key: string, ttl: number): Promise<number>;
15
+ }
16
+
17
+ /**
18
+ * In-memory rate limit store
19
+ */
20
+ class MemoryStore implements RateLimitStore {
21
+ private hits: Map<string, { count: number; resetTime: number }> = new Map();
22
+
23
+ async get(key: string): Promise<number | null> {
24
+ const entry = this.hits.get(key);
25
+ if (!entry) return null;
26
+
27
+ // Clean up expired entries
28
+ if (Date.now() > entry.resetTime) {
29
+ this.hits.delete(key);
30
+ return null;
31
+ }
32
+
33
+ return entry.count;
34
+ }
35
+
36
+ async increment(key: string, ttl: number): Promise<number> {
37
+ const now = Date.now();
38
+ const entry = this.hits.get(key);
39
+
40
+ if (!entry || now > entry.resetTime) {
41
+ const resetTime = now + ttl;
42
+ this.hits.set(key, { count: 1, resetTime });
43
+ return 1;
44
+ }
45
+
46
+ entry.count++;
47
+ return entry.count;
48
+ }
49
+
50
+ // Cleanup old entries periodically
51
+ startCleanup(intervalMs: number = 60000) {
52
+ setInterval(() => {
53
+ const now = Date.now();
54
+ for (const [key, entry] of this.hits.entries()) {
55
+ if (now > entry.resetTime) {
56
+ this.hits.delete(key);
57
+ }
58
+ }
59
+ }, intervalMs);
60
+ }
61
+ }
62
+
63
+ // Create singleton memory store
64
+ const memoryStore = new MemoryStore();
65
+ memoryStore.startCleanup();
66
+
67
+ /**
68
+ * Rate limiting middleware factory
69
+ */
70
+ export function rateLimit(options: RateLimitOptions) {
71
+ const {
72
+ windowMs,
73
+ max,
74
+ message = 'Too many requests, please try again later',
75
+ keyGenerator = (c) => c.req.header('x-forwarded-for') || c.req.header('x-real-ip') || 'unknown',
76
+ } = options;
77
+
78
+ return async (c: Context, next: Next) => {
79
+ const key = `rate-limit:${keyGenerator(c)}`;
80
+ const store = memoryStore;
81
+
82
+ try {
83
+ const hits = await store.increment(key, windowMs);
84
+
85
+ // Set rate limit headers
86
+ c.header('X-RateLimit-Limit', max.toString());
87
+ c.header('X-RateLimit-Remaining', Math.max(0, max - hits).toString());
88
+ c.header('X-RateLimit-Reset', new Date(Date.now() + windowMs).toISOString());
89
+
90
+ if (hits > max) {
91
+ logger.warn('Rate limit exceeded:', { key, hits, max });
92
+ throw new TooManyRequestsError(message);
93
+ }
94
+
95
+ await next();
96
+ } catch (error) {
97
+ if (error instanceof TooManyRequestsError) {
98
+ throw error;
99
+ }
100
+ // If store fails, log but allow request
101
+ logger.error('Rate limit store error:', error);
102
+ await next();
103
+ }
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Common rate limit configurations
109
+ */
110
+ export const rateLimits = {
111
+ // Strict: 10 requests per minute
112
+ strict: rateLimit({
113
+ windowMs: 60 * 1000,
114
+ max: 10,
115
+ }),
116
+
117
+ // Standard: 100 requests per minute
118
+ standard: rateLimit({
119
+ windowMs: 60 * 1000,
120
+ max: 100,
121
+ }),
122
+
123
+ // Lenient: 1000 requests per minute
124
+ lenient: rateLimit({
125
+ windowMs: 60 * 1000,
126
+ max: 1000,
127
+ }),
128
+
129
+ // Auth: 5 login attempts per 15 minutes
130
+ auth: rateLimit({
131
+ windowMs: 15 * 60 * 1000,
132
+ max: 5,
133
+ message: 'Too many login attempts, please try again later',
134
+ }),
135
+ };
@@ -0,0 +1,36 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Hono } from 'hono';
3
+ import { health } from '../health.route.js';
4
+
5
+ describe('Health Route', () => {
6
+ const app = new Hono();
7
+ app.route('/health', health);
8
+
9
+ it('should return 200 OK with health status', async () => {
10
+ const res = await app.request('/health');
11
+ expect(res.status).toBe(200);
12
+
13
+ const json = await res.json();
14
+ expect(json.success).toBe(true);
15
+ expect(json.data).toHaveProperty('status', 'ok');
16
+ expect(json.data).toHaveProperty('timestamp');
17
+ expect(json.data).toHaveProperty('uptime');
18
+ });
19
+
20
+ it('should return valid timestamp', async () => {
21
+ const res = await app.request('/health');
22
+ const json = await res.json();
23
+
24
+ const timestamp = new Date(json.data.timestamp);
25
+ expect(timestamp.getTime()).not.toBeNaN();
26
+ expect(timestamp.getTime()).toBeLessThanOrEqual(Date.now());
27
+ });
28
+
29
+ it('should return positive uptime', async () => {
30
+ const res = await app.request('/health');
31
+ const json = await res.json();
32
+
33
+ expect(json.data.uptime).toBeGreaterThan(0);
34
+ expect(typeof json.data.uptime).toBe('number');
35
+ });
36
+ });
@@ -0,0 +1,14 @@
1
+ import { Hono } from 'hono';
2
+ import { success } from '../lib/response.js';
3
+
4
+ const health = new Hono();
5
+
6
+ health.get('/', (c) => {
7
+ return success(c, {
8
+ status: 'ok',
9
+ timestamp: new Date().toISOString(),
10
+ uptime: process.uptime(),
11
+ });
12
+ });
13
+
14
+ export { health };
@@ -0,0 +1,26 @@
1
+ import type { Hono } from 'hono';
2
+
3
+ export type AppType = Hono;
4
+
5
+ export interface ApiResponse<T = unknown> {
6
+ success: boolean;
7
+ data?: T;
8
+ error?: string;
9
+ message?: string;
10
+ code?: string;
11
+ details?: unknown;
12
+ }
13
+
14
+ export interface PaginationParams {
15
+ page: number;
16
+ limit: number;
17
+ }
18
+
19
+ export interface PaginatedResponse<T> extends ApiResponse<T[]> {
20
+ pagination: {
21
+ page: number;
22
+ limit: number;
23
+ total: number;
24
+ totalPages: number;
25
+ };
26
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*"],
9
+ "exclude": ["node_modules", "dist"]
10
+ }
@@ -0,0 +1,19 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ coverage: {
8
+ provider: 'v8',
9
+ reporter: ['text', 'json', 'html'],
10
+ exclude: [
11
+ 'node_modules/',
12
+ 'dist/',
13
+ '**/*.test.ts',
14
+ '**/*.spec.ts',
15
+ 'vitest.config.ts',
16
+ ],
17
+ },
18
+ },
19
+ });
@@ -0,0 +1,2 @@
1
+ # Database Configuration (MongoDB)
2
+ DATABASE_URL="mongodb://localhost:27017/__PROJECT_NAME__"
@@ -0,0 +1,14 @@
1
+ {
2
+ "dependencies": {
3
+ "@prisma/client": "^6.2.0",
4
+ "@hono/zod-validator": "^0.4.1"
5
+ },
6
+ "devDependencies": {
7
+ "prisma": "^6.2.0"
8
+ },
9
+ "scripts": {
10
+ "prisma:generate": "prisma generate",
11
+ "prisma:push": "prisma db push",
12
+ "prisma:studio": "prisma studio"
13
+ }
14
+ }
@@ -0,0 +1,18 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "mongodb"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id String @id @default(auto()) @map("_id") @db.ObjectId
12
+ email String @unique
13
+ name String?
14
+ createdAt DateTime @default(now())
15
+ updatedAt DateTime @updatedAt
16
+
17
+ @@map("users")
18
+ }
@@ -0,0 +1,43 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { z } from 'zod';
3
+ import { logger } from './logger.js';
4
+
5
+ // Validate database environment variables
6
+ const dbEnvSchema = z.object({
7
+ DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
8
+ });
9
+
10
+ const dbEnv = dbEnvSchema.parse(process.env);
11
+
12
+ const globalForPrisma = globalThis as unknown as {
13
+ prisma: PrismaClient | undefined;
14
+ };
15
+
16
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({
17
+ datasources: {
18
+ db: {
19
+ url: dbEnv.DATABASE_URL,
20
+ },
21
+ },
22
+ log: process.env.NODE_ENV === 'development'
23
+ ? ['query', 'error', 'warn']
24
+ : ['error'],
25
+ });
26
+
27
+ if (process.env.NODE_ENV !== 'production') {
28
+ globalForPrisma.prisma = prisma;
29
+ }
30
+
31
+ // Test connection
32
+ prisma.$connect()
33
+ .then(() => logger.info('Database connected successfully'))
34
+ .catch((error) => {
35
+ logger.error('Database connection failed:', error);
36
+ process.exit(1);
37
+ });
38
+
39
+ // Graceful shutdown
40
+ process.on('beforeExit', async () => {
41
+ await prisma.$disconnect();
42
+ logger.info('Database disconnected');
43
+ });
@@ -0,0 +1,82 @@
1
+ import { Hono } from 'hono';
2
+ import { zValidator } from '@hono/zod-validator';
3
+ import { z } from 'zod';
4
+ import { UserService } from '../services/user.service.js';
5
+ import { success, created, noContent, paginated } from '../lib/response.js';
6
+ import { CreateUserSchema, UpdateUserSchema } from '__PACKAGE_SCOPE__/shared';
7
+
8
+ const users = new Hono();
9
+
10
+ // Query params schema for pagination
11
+ const paginationSchema = z.object({
12
+ page: z.coerce.number().int().min(1).default(1),
13
+ limit: z.coerce.number().int().min(1).max(100).default(10),
14
+ });
15
+
16
+ // Query params schema for search
17
+ const searchSchema = paginationSchema.extend({
18
+ q: z.string().min(1),
19
+ });
20
+
21
+ /**
22
+ * GET /users - List all users
23
+ */
24
+ users.get('/', zValidator('query', paginationSchema), async (c) => {
25
+ const { page, limit } = c.req.valid('query');
26
+ const result = await UserService.getAll(page, limit);
27
+
28
+ return paginated(c, result.users, page, limit, result.total);
29
+ });
30
+
31
+ /**
32
+ * GET /users/search - Search users
33
+ */
34
+ users.get('/search', zValidator('query', searchSchema), async (c) => {
35
+ const { q, page, limit } = c.req.valid('query');
36
+ const result = await UserService.search(q, page, limit);
37
+
38
+ return paginated(c, result.users, page, limit, result.total);
39
+ });
40
+
41
+ /**
42
+ * GET /users/:id - Get user by ID
43
+ */
44
+ users.get('/:id', async (c) => {
45
+ const id = c.req.param('id');
46
+ const user = await UserService.getById(id);
47
+
48
+ return success(c, user);
49
+ });
50
+
51
+ /**
52
+ * POST /users - Create new user
53
+ */
54
+ users.post('/', zValidator('json', CreateUserSchema), async (c) => {
55
+ const data = c.req.valid('json');
56
+ const user = await UserService.create(data);
57
+
58
+ return created(c, user);
59
+ });
60
+
61
+ /**
62
+ * PATCH /users/:id - Update user
63
+ */
64
+ users.patch('/:id', zValidator('json', UpdateUserSchema), async (c) => {
65
+ const id = c.req.param('id');
66
+ const data = c.req.valid('json');
67
+ const user = await UserService.update(id, data);
68
+
69
+ return success(c, user);
70
+ });
71
+
72
+ /**
73
+ * DELETE /users/:id - Delete user
74
+ */
75
+ users.delete('/:id', async (c) => {
76
+ const id = c.req.param('id');
77
+ await UserService.delete(id);
78
+
79
+ return noContent(c);
80
+ });
81
+
82
+ export { users };
@@ -0,0 +1,121 @@
1
+ import { prisma } from '../config/database.js';
2
+ import { NotFoundError, ConflictError } from '../lib/errors.js';
3
+ import type { CreateUser, UpdateUser, User } from '__PACKAGE_SCOPE__/shared';
4
+
5
+ export class UserService {
6
+ /**
7
+ * Get all users with pagination
8
+ */
9
+ static async getAll(page: number = 1, limit: number = 10) {
10
+ const skip = (page - 1) * limit;
11
+
12
+ const [users, total] = await Promise.all([
13
+ prisma.user.findMany({
14
+ skip,
15
+ take: limit,
16
+ orderBy: { createdAt: 'desc' },
17
+ }),
18
+ prisma.user.count(),
19
+ ]);
20
+
21
+ return { users, total, page, limit };
22
+ }
23
+
24
+ /**
25
+ * Get user by ID
26
+ */
27
+ static async getById(id: string): Promise<User> {
28
+ const user = await prisma.user.findUnique({
29
+ where: { id },
30
+ });
31
+
32
+ if (!user) {
33
+ throw new NotFoundError(`User with ID ${id} not found`);
34
+ }
35
+
36
+ return user;
37
+ }
38
+
39
+ /**
40
+ * Get user by email
41
+ */
42
+ static async getByEmail(email: string): Promise<User | null> {
43
+ return prisma.user.findUnique({
44
+ where: { email },
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Create new user
50
+ */
51
+ static async create(data: CreateUser): Promise<User> {
52
+ // Check if email already exists
53
+ const existing = await this.getByEmail(data.email);
54
+ if (existing) {
55
+ throw new ConflictError(`User with email ${data.email} already exists`);
56
+ }
57
+
58
+ return prisma.user.create({
59
+ data,
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Update user
65
+ */
66
+ static async update(id: string, data: UpdateUser): Promise<User> {
67
+ // Verify user exists
68
+ await this.getById(id);
69
+
70
+ // If email is being updated, check it's not taken
71
+ if (data.email) {
72
+ const existing = await this.getByEmail(data.email);
73
+ if (existing && existing.id !== id) {
74
+ throw new ConflictError(`Email ${data.email} is already taken`);
75
+ }
76
+ }
77
+
78
+ return prisma.user.update({
79
+ where: { id },
80
+ data,
81
+ });
82
+ }
83
+
84
+ /**
85
+ * Delete user
86
+ */
87
+ static async delete(id: string): Promise<void> {
88
+ // Verify user exists
89
+ await this.getById(id);
90
+
91
+ await prisma.user.delete({
92
+ where: { id },
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Search users by name or email
98
+ */
99
+ static async search(query: string, page: number = 1, limit: number = 10) {
100
+ const skip = (page - 1) * limit;
101
+
102
+ const where = {
103
+ OR: [
104
+ { name: { contains: query, mode: 'insensitive' as const } },
105
+ { email: { contains: query, mode: 'insensitive' as const } },
106
+ ],
107
+ };
108
+
109
+ const [users, total] = await Promise.all([
110
+ prisma.user.findMany({
111
+ where,
112
+ skip,
113
+ take: limit,
114
+ orderBy: { createdAt: 'desc' },
115
+ }),
116
+ prisma.user.count({ where }),
117
+ ]);
118
+
119
+ return { users, total, page, limit };
120
+ }
121
+ }
@@ -0,0 +1,2 @@
1
+ # Database Configuration (PostgreSQL)
2
+ DATABASE_URL="postgresql://user:password@localhost:5432/__PROJECT_NAME__?schema=public"
@@ -0,0 +1,15 @@
1
+ {
2
+ "dependencies": {
3
+ "@prisma/client": "^6.2.0",
4
+ "@hono/zod-validator": "^0.4.1"
5
+ },
6
+ "devDependencies": {
7
+ "prisma": "^6.2.0"
8
+ },
9
+ "scripts": {
10
+ "prisma:generate": "prisma generate",
11
+ "prisma:migrate": "prisma migrate dev",
12
+ "prisma:studio": "prisma studio",
13
+ "prisma:push": "prisma db push"
14
+ }
15
+ }
@@ -0,0 +1,18 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id String @id @default(cuid())
12
+ email String @unique
13
+ name String?
14
+ createdAt DateTime @default(now())
15
+ updatedAt DateTime @updatedAt
16
+
17
+ @@map("users")
18
+ }
@@ -0,0 +1,43 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { z } from 'zod';
3
+ import { logger } from './logger.js';
4
+
5
+ // Validate database environment variables
6
+ const dbEnvSchema = z.object({
7
+ DATABASE_URL: z.string().min(1, 'DATABASE_URL is required'),
8
+ });
9
+
10
+ const dbEnv = dbEnvSchema.parse(process.env);
11
+
12
+ const globalForPrisma = globalThis as unknown as {
13
+ prisma: PrismaClient | undefined;
14
+ };
15
+
16
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({
17
+ datasources: {
18
+ db: {
19
+ url: dbEnv.DATABASE_URL,
20
+ },
21
+ },
22
+ log: process.env.NODE_ENV === 'development'
23
+ ? ['query', 'error', 'warn']
24
+ : ['error'],
25
+ });
26
+
27
+ if (process.env.NODE_ENV !== 'production') {
28
+ globalForPrisma.prisma = prisma;
29
+ }
30
+
31
+ // Test connection
32
+ prisma.$connect()
33
+ .then(() => logger.info('Database connected successfully'))
34
+ .catch((error) => {
35
+ logger.error('Database connection failed:', error);
36
+ process.exit(1);
37
+ });
38
+
39
+ // Graceful shutdown
40
+ process.on('beforeExit', async () => {
41
+ await prisma.$disconnect();
42
+ logger.info('Database disconnected');
43
+ });
@@ -0,0 +1,82 @@
1
+ import { Hono } from 'hono';
2
+ import { zValidator } from '@hono/zod-validator';
3
+ import { z } from 'zod';
4
+ import { UserService } from '../services/user.service.js';
5
+ import { success, created, noContent, paginated } from '../lib/response.js';
6
+ import { CreateUserSchema, UpdateUserSchema } from '__PACKAGE_SCOPE__/shared';
7
+
8
+ const users = new Hono();
9
+
10
+ // Query params schema for pagination
11
+ const paginationSchema = z.object({
12
+ page: z.coerce.number().int().min(1).default(1),
13
+ limit: z.coerce.number().int().min(1).max(100).default(10),
14
+ });
15
+
16
+ // Query params schema for search
17
+ const searchSchema = paginationSchema.extend({
18
+ q: z.string().min(1),
19
+ });
20
+
21
+ /**
22
+ * GET /users - List all users
23
+ */
24
+ users.get('/', zValidator('query', paginationSchema), async (c) => {
25
+ const { page, limit } = c.req.valid('query');
26
+ const result = await UserService.getAll(page, limit);
27
+
28
+ return paginated(c, result.users, page, limit, result.total);
29
+ });
30
+
31
+ /**
32
+ * GET /users/search - Search users
33
+ */
34
+ users.get('/search', zValidator('query', searchSchema), async (c) => {
35
+ const { q, page, limit } = c.req.valid('query');
36
+ const result = await UserService.search(q, page, limit);
37
+
38
+ return paginated(c, result.users, page, limit, result.total);
39
+ });
40
+
41
+ /**
42
+ * GET /users/:id - Get user by ID
43
+ */
44
+ users.get('/:id', async (c) => {
45
+ const id = c.req.param('id');
46
+ const user = await UserService.getById(id);
47
+
48
+ return success(c, user);
49
+ });
50
+
51
+ /**
52
+ * POST /users - Create new user
53
+ */
54
+ users.post('/', zValidator('json', CreateUserSchema), async (c) => {
55
+ const data = c.req.valid('json');
56
+ const user = await UserService.create(data);
57
+
58
+ return created(c, user);
59
+ });
60
+
61
+ /**
62
+ * PATCH /users/:id - Update user
63
+ */
64
+ users.patch('/:id', zValidator('json', UpdateUserSchema), async (c) => {
65
+ const id = c.req.param('id');
66
+ const data = c.req.valid('json');
67
+ const user = await UserService.update(id, data);
68
+
69
+ return success(c, user);
70
+ });
71
+
72
+ /**
73
+ * DELETE /users/:id - Delete user
74
+ */
75
+ users.delete('/:id', async (c) => {
76
+ const id = c.req.param('id');
77
+ await UserService.delete(id);
78
+
79
+ return noContent(c);
80
+ });
81
+
82
+ export { users };