@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.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/bin/cli.cjs +12 -0
- package/dist/generator.d.ts +59 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +178 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/dist/installer.d.ts +37 -0
- package/dist/installer.d.ts.map +1 -0
- package/dist/installer.js +146 -0
- package/dist/installer.js.map +1 -0
- package/dist/prompts.d.ts +19 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +67 -0
- package/dist/prompts.js.map +1 -0
- package/package.json +59 -0
- package/templates/boilerplate/.env.example +24 -0
- package/templates/boilerplate/.eslintrc.cjs +27 -0
- package/templates/boilerplate/.prettierrc +9 -0
- package/templates/boilerplate/DEPENDENCIES.md +43 -0
- package/templates/boilerplate/README.md +282 -0
- package/templates/boilerplate/docker/.dockerignore +46 -0
- package/templates/boilerplate/docker/Dockerfile +61 -0
- package/templates/boilerplate/docker/docker-compose.yml +68 -0
- package/templates/boilerplate/drizzle/drizzle.config.ts +13 -0
- package/templates/boilerplate/drizzle/schema.ts +22 -0
- package/templates/boilerplate/jest.config.cjs +24 -0
- package/templates/boilerplate/nodemon.json +11 -0
- package/templates/boilerplate/package.json +61 -0
- package/templates/boilerplate/prisma/schema.prisma +0 -0
- package/templates/boilerplate/scripts/generate-module.cjs +397 -0
- package/templates/boilerplate/src/common/middleware/error.middleware.ts +121 -0
- package/templates/boilerplate/src/common/middleware/validation.middleware.ts +50 -0
- package/templates/boilerplate/src/common/utils/http-logger.ts +24 -0
- package/templates/boilerplate/src/common/utils/logger.ts +34 -0
- package/templates/boilerplate/src/common/utils/response-helper.ts +140 -0
- package/templates/boilerplate/src/config/env.ts +24 -0
- package/templates/boilerplate/src/config/index.ts +92 -0
- package/templates/boilerplate/src/database/drizzle.connection.ts +50 -0
- package/templates/boilerplate/src/database/index.ts +20 -0
- package/templates/boilerplate/src/database/mongoose.connection.ts +56 -0
- package/templates/boilerplate/src/database/prisma.connection.ts +50 -0
- package/templates/boilerplate/src/modules/.gitkeep +0 -0
- package/templates/boilerplate/src/server.ts +121 -0
- package/templates/boilerplate/src/types/express.types.ts +29 -0
- package/templates/boilerplate/src/types/index.ts +5 -0
- package/templates/boilerplate/src/types/response.types.ts +54 -0
- 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,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
|
+
}
|