@codeenthusiast09/create-express-app 1.0.0

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +90 -0
  3. package/bin/cli.cjs +12 -0
  4. package/dist/generator.d.ts +59 -0
  5. package/dist/generator.d.ts.map +1 -0
  6. package/dist/generator.js +178 -0
  7. package/dist/generator.js.map +1 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +113 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/installer.d.ts +37 -0
  13. package/dist/installer.d.ts.map +1 -0
  14. package/dist/installer.js +146 -0
  15. package/dist/installer.js.map +1 -0
  16. package/dist/prompts.d.ts +19 -0
  17. package/dist/prompts.d.ts.map +1 -0
  18. package/dist/prompts.js +67 -0
  19. package/dist/prompts.js.map +1 -0
  20. package/package.json +59 -0
  21. package/templates/boilerplate/.env.example +24 -0
  22. package/templates/boilerplate/.eslintrc.cjs +27 -0
  23. package/templates/boilerplate/.prettierrc +9 -0
  24. package/templates/boilerplate/DEPENDENCIES.md +43 -0
  25. package/templates/boilerplate/README.md +282 -0
  26. package/templates/boilerplate/docker/.dockerignore +46 -0
  27. package/templates/boilerplate/docker/Dockerfile +61 -0
  28. package/templates/boilerplate/docker/docker-compose.yml +68 -0
  29. package/templates/boilerplate/drizzle/drizzle.config.ts +13 -0
  30. package/templates/boilerplate/drizzle/schema.ts +22 -0
  31. package/templates/boilerplate/jest.config.cjs +24 -0
  32. package/templates/boilerplate/nodemon.json +11 -0
  33. package/templates/boilerplate/package.json +61 -0
  34. package/templates/boilerplate/prisma/schema.prisma +0 -0
  35. package/templates/boilerplate/scripts/generate-module.cjs +397 -0
  36. package/templates/boilerplate/src/common/middleware/error.middleware.ts +121 -0
  37. package/templates/boilerplate/src/common/middleware/validation.middleware.ts +50 -0
  38. package/templates/boilerplate/src/common/utils/http-logger.ts +24 -0
  39. package/templates/boilerplate/src/common/utils/logger.ts +34 -0
  40. package/templates/boilerplate/src/common/utils/response-helper.ts +140 -0
  41. package/templates/boilerplate/src/config/env.ts +24 -0
  42. package/templates/boilerplate/src/config/index.ts +92 -0
  43. package/templates/boilerplate/src/database/drizzle.connection.ts +50 -0
  44. package/templates/boilerplate/src/database/index.ts +20 -0
  45. package/templates/boilerplate/src/database/mongoose.connection.ts +56 -0
  46. package/templates/boilerplate/src/database/prisma.connection.ts +50 -0
  47. package/templates/boilerplate/src/modules/.gitkeep +0 -0
  48. package/templates/boilerplate/src/server.ts +121 -0
  49. package/templates/boilerplate/src/types/express.types.ts +29 -0
  50. package/templates/boilerplate/src/types/index.ts +5 -0
  51. package/templates/boilerplate/src/types/response.types.ts +54 -0
  52. package/templates/boilerplate/tsconfig.json +72 -0
@@ -0,0 +1,140 @@
1
+ import { Response } from 'express';
2
+ import {
3
+ ApiSuccessResponse,
4
+ ApiFailResponse,
5
+ ApiErrorResponse,
6
+ ErrorDetail,
7
+ } from '@/types/response.types';
8
+ import { config } from '@/config';
9
+
10
+ /**
11
+ * Response Helper
12
+ *
13
+ * Provides consistent, type-safe JSON responses across the API.
14
+ */
15
+ export class ResponseHelper {
16
+ /**
17
+ * Success Response (2xx)
18
+ */
19
+ static success<T>(res: Response, data: T, message = 'Success', statusCode = 200): Response {
20
+ const response = {
21
+ success: true,
22
+ data,
23
+ message,
24
+ timestamp: new Date().toISOString(),
25
+ } satisfies ApiSuccessResponse<T>;
26
+ return res.status(statusCode).json(response);
27
+ }
28
+
29
+ /**
30
+ * Client Error Response (4xx)
31
+ * Use for validation errors, not found, unauthorized, etc.
32
+ */
33
+ static fail(
34
+ res: Response,
35
+ code: string,
36
+ message: string,
37
+ statusCode = 400,
38
+ details?: ErrorDetail[],
39
+ ): Response {
40
+ const response = {
41
+ success: false,
42
+ error: {
43
+ code,
44
+ message,
45
+ details,
46
+ },
47
+ timestamp: new Date().toISOString(),
48
+ } satisfies ApiFailResponse;
49
+ return res.status(statusCode).json(response);
50
+ }
51
+
52
+ /**
53
+ * Server Error Response (5xx)
54
+ * Use for unexpected server errors
55
+ */
56
+ static error(
57
+ res: Response,
58
+ code: string,
59
+ message: string,
60
+ statusCode = 500,
61
+ error?: Error,
62
+ ): Response {
63
+ const response = {
64
+ success: false,
65
+ error: {
66
+ code,
67
+ message,
68
+ // Only include stack trace in development
69
+ stack: config.nodeEnv === 'development' ? error?.stack : undefined,
70
+ },
71
+ timestamp: new Date().toISOString(),
72
+ } satisfies ApiErrorResponse;
73
+ return res.status(statusCode).json(response);
74
+ }
75
+
76
+ // ========== Convenience Methods ==========
77
+
78
+ /**
79
+ * 201 Created
80
+ */
81
+ static created<T>(res: Response, data: T, message = 'Resource created') {
82
+ return ResponseHelper.success(res, data, message, 201);
83
+ }
84
+
85
+ /**
86
+ * 204 No Content
87
+ */
88
+ static noContent(res: Response) {
89
+ return res.status(204).send();
90
+ }
91
+
92
+ /**
93
+ * 400 Bad Request (with validation errors)
94
+ */
95
+ static badRequest(res: Response, message: string, details?: ErrorDetail[]) {
96
+ return ResponseHelper.fail(res, 'BAD_REQUEST', message, 400, details);
97
+ }
98
+
99
+ /**
100
+ * 401 Unauthorized
101
+ */
102
+ static unauthorized(res: Response, message = 'Authentication required') {
103
+ return ResponseHelper.fail(res, 'UNAUTHORIZED', message, 401);
104
+ }
105
+
106
+ /**
107
+ * 403 Forbidden
108
+ */
109
+ static forbidden(res: Response, message = 'Access denied') {
110
+ return ResponseHelper.fail(res, 'FORBIDDEN', message, 403);
111
+ }
112
+
113
+ /**
114
+ * 404 Not Found
115
+ */
116
+ static notFound(res: Response, message = 'Resource not found') {
117
+ return ResponseHelper.fail(res, 'NOT_FOUND', message, 404);
118
+ }
119
+
120
+ /**
121
+ * 409 Conflict
122
+ */
123
+ static conflict(res: Response, message: string) {
124
+ return ResponseHelper.fail(res, 'CONFLICT', message, 409);
125
+ }
126
+
127
+ /**
128
+ * 422 Unprocessable Entity (validation errors)
129
+ */
130
+ static validationError(res: Response, details: ErrorDetail[]) {
131
+ return ResponseHelper.fail(res, 'VALIDATION_ERROR', 'Validation failed', 422, details);
132
+ }
133
+
134
+ /**
135
+ * 500 Internal Server Error
136
+ */
137
+ static internalError(res: Response, message = 'Internal server error', error?: Error) {
138
+ return ResponseHelper.error(res, 'INTERNAL_SERVER_ERROR', message, 500, error);
139
+ }
140
+ }
@@ -0,0 +1,24 @@
1
+ import dotenv from 'dotenv';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Load environment variables from .env file
6
+ *
7
+ * This MUST be imported before any other config files
8
+ * to ensure environment variables are available.
9
+ */
10
+
11
+ // Load .env from project root
12
+ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
13
+
14
+ // Validate required environment variables
15
+ const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
16
+
17
+ for (const envVar of requiredEnvVars) {
18
+ if (!process.env[envVar]) {
19
+ throw new Error(`Missing required environment variable: ${envVar}`);
20
+ }
21
+ }
22
+
23
+ // Export nothing - this file is just for side effects
24
+ export {};
@@ -0,0 +1,92 @@
1
+ import './env';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * Configuration Schema
6
+ *
7
+ * This defines what environment variables we need and validates them.
8
+ * If validation fails, the app won't start and will show you what's wrong.
9
+ */
10
+ const configSchema = z.object({
11
+ // Application settings
12
+ nodeEnv: z.enum(['development', 'production', 'test']).default('development'),
13
+ port: z.number().int().positive().default(3000),
14
+
15
+ // Database
16
+ database: z.object({
17
+ url: z.string().min(1, 'DATABASE_URL is required'),
18
+ }),
19
+
20
+ // JWT Authentication
21
+ jwt: z.object({
22
+ secret: z.string().min(32, 'JWT_SECRET must be at least 32 characters for security'),
23
+ expiresIn: z.string().default('7d'),
24
+ }),
25
+
26
+ // CORS Configuration
27
+ cors: z.object({
28
+ origin: z.string().or(z.array(z.string())).default('*'),
29
+ credentials: z.boolean().default(true),
30
+ }),
31
+
32
+ // Logging
33
+ logging: z.object({
34
+ level: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
35
+ }),
36
+ });
37
+
38
+ /**
39
+ * Parse and Validate Configuration
40
+ *
41
+ * This reads from process.env, validates it, and returns a type-safe config object.
42
+ */
43
+ const parseConfig = () => {
44
+ const rawConfig = {
45
+ nodeEnv: process.env.NODE_ENV || 'development',
46
+ port: parseInt(process.env.PORT || '3000', 10),
47
+ database: {
48
+ url: process.env.DATABASE_URL || '',
49
+ },
50
+ jwt: {
51
+ secret: process.env.JWT_SECRET || '',
52
+ expiresIn: process.env.JWT_EXPIRES_IN || '7d',
53
+ },
54
+ cors: {
55
+ origin: process.env.CORS_ORIGIN || '*',
56
+ credentials: process.env.CORS_CREDENTIALS === 'true',
57
+ },
58
+ logging: {
59
+ level: (process.env.LOG_LEVEL || 'info') as 'info',
60
+ },
61
+ };
62
+
63
+ try {
64
+ return configSchema.parse(rawConfig);
65
+ } catch (error) {
66
+ if (error instanceof z.ZodError) {
67
+ console.error('❌ Invalid environment variables:');
68
+ error.errors.forEach((err) => {
69
+ console.error(` - ${err.path.join('.')}: ${err.message}`);
70
+ });
71
+ process.exit(1);
72
+ }
73
+ throw error;
74
+ }
75
+ };
76
+
77
+ /**
78
+ * Validated Configuration
79
+ *
80
+ * Import this anywhere in your app to access config.
81
+ * TypeScript knows the exact shape and will autocomplete!
82
+ *
83
+ * Usage:
84
+ * import { config } from '@/config';
85
+ * const port = config.port; // TypeScript knows this is a number
86
+ */
87
+ export const config = parseConfig();
88
+
89
+ /**
90
+ * Configuration Type
91
+ */
92
+ export type Config = z.infer<typeof configSchema>;
@@ -0,0 +1,50 @@
1
+ import { drizzle } from 'drizzle-orm/node-postgres';
2
+ import { Pool } from 'pg';
3
+ import { config } from '@/config';
4
+ import { logger } from '@/common/utils/logger';
5
+
6
+ /**
7
+ * PostgreSQL Connection (Drizzle)
8
+ */
9
+
10
+ // Create connection pool
11
+ const pool = new Pool({
12
+ connectionString: config.database.url,
13
+ max: 10,
14
+ min: 2,
15
+ idleTimeoutMillis: 30000,
16
+ connectionTimeoutMillis: 5000,
17
+ });
18
+
19
+ // Initialize Drizzle
20
+ export const db = drizzle(pool);
21
+
22
+ export const connectDatabase = async (): Promise<void> => {
23
+ try {
24
+ // Test connection
25
+ await pool.query('SELECT NOW()');
26
+ logger.info('󰪩 PostgreSQL connected successfully (Drizzle)');
27
+ } catch (error) {
28
+ logger.error('󱙀 PostgreSQL connection error:', error);
29
+ process.exit(1);
30
+ }
31
+ };
32
+
33
+ export const disconnectDatabase = async (): Promise<void> => {
34
+ try {
35
+ await pool.end();
36
+ logger.info('󱙀 PostgreSQL disconnected');
37
+ } catch (error) {
38
+ logger.error('󱙀 Error disconnecting from PostgreSQL:', error);
39
+ }
40
+ };
41
+
42
+ // Graceful shutdown
43
+ const gracefulShutdown = async (signal: string) => {
44
+ logger.info(`${signal} received. Closing PostgreSQL connection...`);
45
+ await disconnectDatabase();
46
+ process.exit(0);
47
+ };
48
+
49
+ process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
50
+ process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Database Connection Export
3
+ *
4
+ * This file exports the database connection based on your choice.
5
+ * The CLI will automatically update this file during project generation.
6
+ *
7
+ * Available options:
8
+ * - MongoDB with Mongoose
9
+ * - PostgreSQL with Prisma
10
+ * - PostgreSQL with Drizzle
11
+ */
12
+
13
+ // Default: Mongoose (MongoDB)
14
+ export * from './mongoose.connection';
15
+
16
+ // Uncomment for Prisma (PostgreSQL)
17
+ // export * from './prisma.connection';
18
+
19
+ // Uncomment for Drizzle (PostgreSQL)
20
+ // export * from './drizzle.connection';
@@ -0,0 +1,56 @@
1
+ import mongoose from 'mongoose';
2
+ import { config } from '@/config';
3
+ import { logger } from '@/common/utils/logger';
4
+
5
+ /**
6
+ * MongoDB Connection (Mongoose)
7
+ */
8
+
9
+ export const connectDatabase = async (): Promise<void> => {
10
+ try {
11
+ const options: mongoose.ConnectOptions = {
12
+ maxPoolSize: 10,
13
+ minPoolSize: 2,
14
+ serverSelectionTimeoutMS: 5000,
15
+ socketTimeoutMS: 45000,
16
+ };
17
+
18
+ await mongoose.connect(config.database.url, options);
19
+ logger.info('󰆼 MongoDB connected successfully');
20
+ } catch (error) {
21
+ logger.error('󱙀 MongoDB connection error:', error);
22
+ process.exit(1);
23
+ }
24
+ };
25
+
26
+ export const disconnectDatabase = async (): Promise<void> => {
27
+ try {
28
+ await mongoose.disconnect();
29
+ logger.info('󰆼 MongoDB disconnected');
30
+ } catch (error) {
31
+ logger.error('󱙀 Error disconnecting from MongoDB:', error);
32
+ }
33
+ };
34
+
35
+ // Connection event listeners
36
+ mongoose.connection.on('connected', () => {
37
+ logger.info('󰪩 Mongoose connected to MongoDB');
38
+ });
39
+
40
+ mongoose.connection.on('disconnected', () => {
41
+ logger.warn('󱙀 Mongoose disconnected from MongoDB');
42
+ });
43
+
44
+ mongoose.connection.on('error', (error) => {
45
+ logger.error('󱙀 MongoDB connection error:', error);
46
+ });
47
+
48
+ // Graceful shutdown
49
+ const gracefulShutdown = async (signal: string) => {
50
+ logger.info(`󰆼 ${signal} received. Closing MongoDB connection...`);
51
+ await disconnectDatabase();
52
+ process.exit(0);
53
+ };
54
+
55
+ process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
56
+ process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
@@ -0,0 +1,50 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ import { logger } from '@/common/utils/logger';
3
+ import { config } from '@/config';
4
+
5
+ /**
6
+ * PostgreSQL Connection (Prisma)
7
+ */
8
+
9
+ // Singleton pattern to prevent multiple instances
10
+ const globalForPrisma = global as unknown as { prisma: PrismaClient };
11
+
12
+ export const prisma =
13
+ globalForPrisma.prisma ||
14
+ new PrismaClient({
15
+ log: config.nodeEnv === 'development' ? ['query', 'error', 'warn'] : ['error'],
16
+ errorFormat: config.nodeEnv === 'development' ? 'pretty' : 'minimal',
17
+ });
18
+
19
+ if (config.nodeEnv !== 'production') {
20
+ globalForPrisma.prisma = prisma;
21
+ }
22
+
23
+ export const connectDatabase = async (): Promise<void> => {
24
+ try {
25
+ await prisma.$connect();
26
+ logger.info('󰪩 PostgreSQL connected successfully (Prisma)');
27
+ } catch (error) {
28
+ logger.error('󱙀 PostgreSQL connection error:', error);
29
+ process.exit(1);
30
+ }
31
+ };
32
+
33
+ export const disconnectDatabase = async (): Promise<void> => {
34
+ try {
35
+ await prisma.$disconnect();
36
+ logger.info('󱙀 PostgreSQL disconnected');
37
+ } catch (error) {
38
+ logger.error('󱙀 Error disconnecting from PostgreSQL:', error);
39
+ }
40
+ };
41
+
42
+ // Graceful shutdown
43
+ const gracefulShutdown = async (signal: string) => {
44
+ logger.info(`${signal} received. Closing PostgreSQL connection...`);
45
+ await disconnectDatabase();
46
+ process.exit(0);
47
+ };
48
+
49
+ process.on('SIGINT', () => void gracefulShutdown('SIGINT'));
50
+ process.on('SIGTERM', () => void gracefulShutdown('SIGTERM'));
File without changes
@@ -0,0 +1,121 @@
1
+ import { config } from './config';
2
+ import express, { Application } from 'express';
3
+ import cors from 'cors';
4
+ import { logger } from './common/utils/logger';
5
+ import { httpLogger } from './common/utils/http-logger';
6
+ import { errorHandler, notFoundHandler } from './common/middleware/error.middleware';
7
+ import { connectDatabase } from './database';
8
+
9
+ /**
10
+ * Express Application Setup
11
+ */
12
+
13
+ const app: Application = express();
14
+
15
+ /**
16
+ * Middleware
17
+ */
18
+ app.use(
19
+ cors({
20
+ origin: config.cors.origin,
21
+ credentials: config.cors.credentials,
22
+ }),
23
+ );
24
+
25
+ app.use(express.json({ limit: '10mb' }));
26
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
27
+ app.use(httpLogger);
28
+
29
+ /**
30
+ * Health Check Endpoint
31
+ */
32
+ app.get('/health', (_req, res) => {
33
+ res.json({
34
+ status: 'ok',
35
+ timestamp: new Date().toISOString(),
36
+ uptime: process.uptime(),
37
+ environment: config.nodeEnv,
38
+ });
39
+ });
40
+
41
+ /**
42
+ * Root Endpoint
43
+ */
44
+ app.get('/', (_req, res) => {
45
+ res.json({
46
+ message: 'Hello World!',
47
+ version: '1.0.0',
48
+ });
49
+ });
50
+
51
+ /**
52
+ * API Routes
53
+ *
54
+ * Add your routes here:
55
+ * app.use('/api/users', userRoutes);
56
+ * app.use('/api/posts', postRoutes);
57
+ */
58
+
59
+ /**
60
+ * Error Handling (must be last)
61
+ */
62
+ app.use(notFoundHandler);
63
+ app.use(errorHandler);
64
+
65
+ /**
66
+ * Start Server
67
+ */
68
+ const startServer = async (): Promise<void> => {
69
+ try {
70
+ await connectDatabase();
71
+
72
+ const PORT = config.port;
73
+ app.listen(PORT, () => {
74
+ if (config.nodeEnv === 'development') {
75
+ logger.info(`
76
+ ╔════════════════════════════════════════╗
77
+ ║ 🚀 Server is running! ║
78
+ ║ 📍 http://localhost:${PORT} ║
79
+ ║ 🌍 Environment: ${config.nodeEnv.padEnd(10)} ║
80
+ ╚════════════════════════════════════════╝
81
+ `);
82
+ } else {
83
+ // Production: Simple, JSON-friendly message
84
+ logger.info({
85
+ message: 'Server is running',
86
+ port: PORT,
87
+ environment: config.nodeEnv,
88
+ url: `http://localhost:${PORT}`,
89
+ });
90
+ }
91
+ });
92
+ } catch (error) {
93
+ logger.error('Failed to start server:', error);
94
+ process.exit(1);
95
+ }
96
+ };
97
+
98
+ // Graceful shutdown
99
+ const gracefulShutdown = (signal: string) => {
100
+ logger.info(`${signal} received. Starting graceful shutdown...`);
101
+ process.exit(0);
102
+ };
103
+
104
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
105
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
106
+ process.on('unhandledRejection', (reason: Error) => {
107
+ logger.error('Unhandled Promise Rejection:', reason);
108
+ process.exit(1);
109
+ });
110
+ process.on('uncaughtException', (error: Error) => {
111
+ logger.error('Uncaught Exception:', error);
112
+ process.exit(1);
113
+ });
114
+
115
+ startServer().catch((error) => {
116
+ logger.error('Failed to start server:', error);
117
+
118
+ process.exit(1);
119
+ });
120
+
121
+ export default app;
@@ -0,0 +1,29 @@
1
+ import { Request } from 'express';
2
+
3
+ /**
4
+ * Authenticated User
5
+ */
6
+ export interface AuthUser {
7
+ id: string;
8
+ email: string;
9
+ role?: string;
10
+ }
11
+
12
+ /**
13
+ * Extend Express Request globally
14
+ */
15
+ declare global {
16
+ // eslint-disable-next-line @typescript-eslint/no-namespace
17
+ namespace Express {
18
+ interface Request {
19
+ user?: AuthUser;
20
+ }
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Type-safe Request with User (for protected routes)
26
+ */
27
+ export interface AuthenticatedRequest extends Request {
28
+ user: AuthUser;
29
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Types Index
3
+ */
4
+ export * from './express.types';
5
+ export * from './response.types';
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Base API Response
3
+ */
4
+ interface BaseResponse {
5
+ success: boolean;
6
+ message?: string;
7
+ timestamp?: string; // ISO 8601
8
+ }
9
+
10
+ /**
11
+ * Successful API Response
12
+ */
13
+ export interface ApiSuccessResponse<T = unknown> extends BaseResponse {
14
+ success: true;
15
+ data: T;
16
+ }
17
+
18
+ /**
19
+ * Failed API Response (Client Errors - 4xx)
20
+ */
21
+ export interface ApiFailResponse extends BaseResponse {
22
+ success: false;
23
+ error: {
24
+ code: string; // e.g., "VALIDATION_ERROR", "NOT_FOUND"
25
+ message: string;
26
+ details?: ErrorDetail[]; // For validation errors
27
+ };
28
+ }
29
+
30
+ /**
31
+ * Error API Response (Server Errors - 5xx)
32
+ */
33
+ export interface ApiErrorResponse extends BaseResponse {
34
+ success: false;
35
+ error: {
36
+ code: string; // e.g., "INTERNAL_SERVER_ERROR"
37
+ message: string;
38
+ stack?: string; // Only in development
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Union of all possible responses
44
+ */
45
+ export type ApiResponse<T = unknown> = ApiSuccessResponse<T> | ApiFailResponse | ApiErrorResponse;
46
+
47
+ /**
48
+ * Error detail for validation errors
49
+ */
50
+ export interface ErrorDetail {
51
+ field: string;
52
+ message: string;
53
+ code?: string; // e.g., "REQUIRED", "INVALID_FORMAT"
54
+ }