create-theta-code 1.0.1

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 (68) hide show
  1. package/bin/create.js +9 -0
  2. package/package.json +34 -0
  3. package/src/cli.js +21 -0
  4. package/src/generators/scaffoldProject.js +46 -0
  5. package/src/prompts/getProjectName.js +30 -0
  6. package/src/prompts/getTemplateChoice.js +39 -0
  7. package/src/utils/logger.js +29 -0
  8. package/templates/mongo-js/.env +12 -0
  9. package/templates/mongo-js/.env.example +13 -0
  10. package/templates/mongo-js/.eslintrc.json +24 -0
  11. package/templates/mongo-js/.prettierrc +9 -0
  12. package/templates/mongo-js/README.md +429 -0
  13. package/templates/mongo-js/_env.example +13 -0
  14. package/templates/mongo-js/_gitignore +22 -0
  15. package/templates/mongo-js/package-lock.json +4671 -0
  16. package/templates/mongo-js/package.json +48 -0
  17. package/templates/mongo-js/server.js +67 -0
  18. package/templates/mongo-js/src/config/app.config.js +72 -0
  19. package/templates/mongo-js/src/config/db.config.js +32 -0
  20. package/templates/mongo-js/src/config/env.config.js +49 -0
  21. package/templates/mongo-js/src/config/rateLimiter.config.js +32 -0
  22. package/templates/mongo-js/src/middlewares/auth.middleware.js +20 -0
  23. package/templates/mongo-js/src/middlewares/error.middleware.js +61 -0
  24. package/templates/mongo-js/src/middlewares/notFound.middleware.js +11 -0
  25. package/templates/mongo-js/src/middlewares/requestId.middleware.js +10 -0
  26. package/templates/mongo-js/src/middlewares/requireRole.middleware.js +13 -0
  27. package/templates/mongo-js/src/middlewares/validate.middleware.js +21 -0
  28. package/templates/mongo-js/src/modules/user/user.controller.js +88 -0
  29. package/templates/mongo-js/src/modules/user/user.model.js +45 -0
  30. package/templates/mongo-js/src/modules/user/user.repository.js +47 -0
  31. package/templates/mongo-js/src/modules/user/user.routes.js +32 -0
  32. package/templates/mongo-js/src/modules/user/user.service.js +87 -0
  33. package/templates/mongo-js/src/modules/user/user.validator.js +28 -0
  34. package/templates/mongo-js/src/utils/AppError.js +15 -0
  35. package/templates/mongo-js/src/utils/apiResponse.js +23 -0
  36. package/templates/mongo-js/src/utils/asyncHandler.js +7 -0
  37. package/templates/mongo-js/src/utils/constants.js +16 -0
  38. package/templates/mongo-js/src/utils/jwt.utils.js +40 -0
  39. package/templates/mongo-js/tests/integration/user.routes.test.js +111 -0
  40. package/templates/mongo-js/tests/unit/user.service.test.js +96 -0
  41. package/templates/pg-js/.eslintrc.json +24 -0
  42. package/templates/pg-js/.prettierrc +9 -0
  43. package/templates/pg-js/_env.example +7 -0
  44. package/templates/pg-js/_gitignore +20 -0
  45. package/templates/pg-js/package.json +50 -0
  46. package/templates/pg-js/prisma/schema.prisma +23 -0
  47. package/templates/pg-js/server.js +63 -0
  48. package/templates/pg-js/src/config/app.config.js +48 -0
  49. package/templates/pg-js/src/config/db.config.js +30 -0
  50. package/templates/pg-js/src/config/env.config.js +36 -0
  51. package/templates/pg-js/src/config/rateLimiter.config.js +22 -0
  52. package/templates/pg-js/src/middlewares/auth.middleware.js +32 -0
  53. package/templates/pg-js/src/middlewares/error.middleware.js +50 -0
  54. package/templates/pg-js/src/middlewares/notFound.middleware.js +11 -0
  55. package/templates/pg-js/src/middlewares/validate.middleware.js +21 -0
  56. package/templates/pg-js/src/modules/user/user.controller.js +57 -0
  57. package/templates/pg-js/src/modules/user/user.model.js +20 -0
  58. package/templates/pg-js/src/modules/user/user.repository.js +105 -0
  59. package/templates/pg-js/src/modules/user/user.routes.js +27 -0
  60. package/templates/pg-js/src/modules/user/user.service.js +81 -0
  61. package/templates/pg-js/src/modules/user/user.validator.js +22 -0
  62. package/templates/pg-js/src/utils/AppError.js +14 -0
  63. package/templates/pg-js/src/utils/apiResponse.js +23 -0
  64. package/templates/pg-js/src/utils/asyncHandler.js +7 -0
  65. package/templates/pg-js/src/utils/constants.js +24 -0
  66. package/templates/pg-js/src/utils/jwt.utils.js +39 -0
  67. package/templates/pg-js/tests/integration/user.routes.test.js +95 -0
  68. package/templates/pg-js/tests/unit/user.service.test.js +96 -0
@@ -0,0 +1,87 @@
1
+ // src/modules/user/user.service.js — Business logic only. Never DB queries, never HTTP.
2
+
3
+ import bcryptjs from 'bcryptjs';
4
+ import { AppError } from '../../utils/AppError.js';
5
+ import { HTTP_STATUS } from '../../utils/constants.js';
6
+ import envConfig from '../../config/env.config.js';
7
+ import { signToken, signRefreshToken } from '../../utils/jwt.utils.js';
8
+ import * as userRepository from './user.repository.js';
9
+
10
+ async function register(email, firstName, lastName, password) {
11
+ // Check if user exists
12
+ const existingUser = await userRepository.findByEmailPublic(email);
13
+ if (existingUser) {
14
+ throw new AppError('Email already in use', HTTP_STATUS.CONFLICT);
15
+ }
16
+
17
+ // Hash password
18
+ const hashedPassword = await bcryptjs.hash(password, envConfig.BCRYPT_SALT_ROUNDS);
19
+
20
+ // Create user
21
+ const user = await userRepository.create({
22
+ email,
23
+ firstName,
24
+ lastName,
25
+ password: hashedPassword,
26
+ });
27
+
28
+ return user;
29
+ }
30
+
31
+ async function login(email, password) {
32
+ // Find user with password
33
+ const user = await userRepository.findByEmail(email);
34
+ if (!user) {
35
+ throw new AppError('Invalid email or password', HTTP_STATUS.UNAUTHORIZED);
36
+ }
37
+
38
+ // Verify password
39
+ const isPasswordValid = await bcryptjs.compare(password, user.password);
40
+ if (!isPasswordValid) {
41
+ throw new AppError('Invalid email or password', HTTP_STATUS.UNAUTHORIZED);
42
+ }
43
+
44
+ // Generate tokens with role included
45
+ const token = signToken({ id: user._id, email: user.email, role: user.role });
46
+ const refreshToken = signRefreshToken({ id: user._id });
47
+
48
+ // Fetch clean user object - no manual delete needed
49
+ const cleanUser = await userRepository.findById(user._id);
50
+
51
+ return {
52
+ user: cleanUser,
53
+ token,
54
+ refreshToken,
55
+ };
56
+ }
57
+
58
+ async function getAllUsers(limit = 10, skip = 0) {
59
+ const users = await userRepository.findAll(limit, skip);
60
+ return users;
61
+ }
62
+
63
+ async function getUserById(id) {
64
+ const user = await userRepository.findById(id);
65
+ if (!user) {
66
+ throw new AppError('User not found', HTTP_STATUS.NOT_FOUND);
67
+ }
68
+ return user;
69
+ }
70
+
71
+ async function updateUser(id, updateData) {
72
+ const user = await userRepository.updateById(id, updateData);
73
+ if (!user) {
74
+ throw new AppError('User not found', HTTP_STATUS.NOT_FOUND);
75
+ }
76
+ return user;
77
+ }
78
+
79
+ async function deleteUser(id) {
80
+ const user = await userRepository.deleteById(id);
81
+ if (!user) {
82
+ throw new AppError('User not found', HTTP_STATUS.NOT_FOUND);
83
+ }
84
+ return true;
85
+ }
86
+
87
+ export { register, login, getAllUsers, getUserById, updateUser, deleteUser };
@@ -0,0 +1,28 @@
1
+ // src/modules/user/user.validator.js — Zod validation schemas. Never validate in controller.
2
+
3
+ import { z } from 'zod';
4
+
5
+ const registerSchema = z.object({
6
+ email: z.string().email('Invalid email format'),
7
+ firstName: z.string().min(2, 'First name must be at least 2 characters').max(50),
8
+ lastName: z.string().min(2, 'Last name must be at least 2 characters').max(50),
9
+ password: z
10
+ .string()
11
+ .min(8, 'Password must be at least 8 characters')
12
+ .regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
13
+ .regex(/[a-z]/, 'Password must contain at least one lowercase letter')
14
+ .regex(/[0-9]/, 'Password must contain at least one number')
15
+ .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character'),
16
+ });
17
+
18
+ const loginSchema = z.object({
19
+ email: z.string().email('Invalid email format'),
20
+ password: z.string().min(1, 'Password is required'),
21
+ });
22
+
23
+ const updateUserSchema = z.object({
24
+ firstName: z.string().min(2).max(50).optional(),
25
+ lastName: z.string().min(2).max(50).optional(),
26
+ });
27
+
28
+ export { registerSchema, loginSchema, updateUserSchema };
@@ -0,0 +1,15 @@
1
+ // src/utils/AppError.js — Custom Error class for operational errors. All errors inherit this.
2
+
3
+ class AppError extends Error {
4
+ constructor(message, statusCode, isOperational = true) {
5
+ super(message);
6
+ this.statusCode = statusCode;
7
+ this.status = String(statusCode).startsWith('4') ? 'fail' : 'error';
8
+ this.isOperational = isOperational;
9
+ this.timestamp = new Date().toISOString();
10
+
11
+ Error.captureStackTrace(this, this.constructor);
12
+ }
13
+ }
14
+
15
+ export { AppError };
@@ -0,0 +1,23 @@
1
+ // src/utils/apiResponse.js — Standardized success/error response formatting. Always use these.
2
+
3
+ import { HTTP_STATUS } from './constants.js';
4
+
5
+ function sendSuccess(res, statusCode = HTTP_STATUS.OK, message = 'Success', data = null) {
6
+ return res.status(statusCode).json({
7
+ success: true,
8
+ message,
9
+ data,
10
+ timestamp: new Date().toISOString(),
11
+ });
12
+ }
13
+
14
+ function sendError(res, statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR, message = 'Internal Server Error', data = null) {
15
+ return res.status(statusCode).json({
16
+ success: false,
17
+ message,
18
+ data,
19
+ timestamp: new Date().toISOString(),
20
+ });
21
+ }
22
+
23
+ export { sendSuccess, sendError };
@@ -0,0 +1,7 @@
1
+ // src/utils/asyncHandler.js — Wraps async controller/service functions. Zero try/catch in controllers.
2
+
3
+ const asyncHandler = (fn) => (req, res, next) => {
4
+ return Promise.resolve(fn(req, res, next)).catch(next);
5
+ };
6
+
7
+ export { asyncHandler };
@@ -0,0 +1,16 @@
1
+ // src/utils/constants.js — All app constants. No hardcoded strings anywhere.
2
+
3
+ const HTTP_STATUS = {
4
+ OK: 200,
5
+ CREATED: 201,
6
+ BAD_REQUEST: 400,
7
+ UNAUTHORIZED: 401,
8
+ FORBIDDEN: 403,
9
+ NOT_FOUND: 404,
10
+ CONFLICT: 409,
11
+ INTERNAL_SERVER_ERROR: 500,
12
+ };
13
+
14
+ const REQUEST_BODY_LIMIT = '10kb';
15
+
16
+ export { HTTP_STATUS, REQUEST_BODY_LIMIT };
@@ -0,0 +1,40 @@
1
+ // src/utils/jwt.utils.js — JWT token operations only. Never inline JWT logic anywhere.
2
+
3
+ import jwt from 'jsonwebtoken';
4
+ import { AppError } from './AppError.js';
5
+ import { HTTP_STATUS } from './constants.js';
6
+ import envConfig from '../config/env.config.js';
7
+
8
+ function signToken(payload, expiresIn = envConfig.JWT_EXPIRES_IN) {
9
+ const secret = envConfig.JWT_SECRET;
10
+ if (!secret) {
11
+ throw new AppError('JWT_SECRET not configured', HTTP_STATUS.INTERNAL_SERVER_ERROR, false);
12
+ }
13
+ return jwt.sign(payload, secret, { expiresIn });
14
+ }
15
+
16
+ function signRefreshToken(payload, expiresIn = envConfig.JWT_REFRESH_EXPIRES_IN) {
17
+ const secret = envConfig.JWT_REFRESH_SECRET;
18
+ if (!secret) {
19
+ throw new AppError('JWT_REFRESH_SECRET not configured', HTTP_STATUS.INTERNAL_SERVER_ERROR, false);
20
+ }
21
+ return jwt.sign(payload, secret, { expiresIn });
22
+ }
23
+
24
+ function verifyToken(token) {
25
+ const secret = envConfig.JWT_SECRET;
26
+ if (!secret) {
27
+ throw new AppError('JWT_SECRET not configured', HTTP_STATUS.INTERNAL_SERVER_ERROR, false);
28
+ }
29
+ return jwt.verify(token, secret);
30
+ }
31
+
32
+ function verifyRefreshToken(token) {
33
+ const secret = envConfig.JWT_REFRESH_SECRET;
34
+ if (!secret) {
35
+ throw new AppError('JWT_REFRESH_SECRET not configured', HTTP_STATUS.INTERNAL_SERVER_ERROR, false);
36
+ }
37
+ return jwt.verify(token, secret);
38
+ }
39
+
40
+ export { signToken, signRefreshToken, verifyToken, verifyRefreshToken };
@@ -0,0 +1,111 @@
1
+ // tests/integration/user.routes.test.js — Integration tests for HTTP layer. Real app, mocked DB.
2
+
3
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
4
+ import request from 'supertest';
5
+ import bcryptjs from 'bcryptjs';
6
+ import { createApp } from '../../src/config/app.config.js';
7
+ import * as userRepository from '../../src/modules/user/user.repository.js';
8
+
9
+ vi.mock('../../src/modules/user/user.repository.js');
10
+ vi.mock('bcryptjs');
11
+
12
+ describe('User Routes', () => {
13
+ let app;
14
+
15
+ beforeEach(() => {
16
+ app = createApp();
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ describe('POST /api/v1/users/register', () => {
21
+ it('should register a new user', async () => {
22
+ vi.mocked(userRepository.findByEmailPublic).mockResolvedValueOnce(null);
23
+ vi.mocked(userRepository.create).mockResolvedValueOnce({
24
+ _id: '123',
25
+ email: 'test@example.com',
26
+ firstName: 'John',
27
+ lastName: 'Doe',
28
+ });
29
+
30
+ const response = await request(app)
31
+ .post('/api/v1/users/register')
32
+ .send({
33
+ email: 'test@example.com',
34
+ firstName: 'John',
35
+ lastName: 'Doe',
36
+ password: 'Password123!',
37
+ });
38
+
39
+ expect(response.status).toBe(201);
40
+ expect(response.body).toHaveProperty('success', true);
41
+ expect(response.body).toHaveProperty('data.user');
42
+ });
43
+
44
+ it('should return 400 on validation error', async () => {
45
+ const response = await request(app)
46
+ .post('/api/v1/users/register')
47
+ .send({
48
+ email: 'invalid-email',
49
+ firstName: 'John',
50
+ lastName: 'Doe',
51
+ password: 'pass',
52
+ });
53
+
54
+ expect(response.status).toBe(400);
55
+ expect(response.body).toHaveProperty('success', false);
56
+ });
57
+ });
58
+
59
+ describe('POST /api/v1/users/login', () => {
60
+ it('should login successfully', async () => {
61
+ const user = {
62
+ _id: '123',
63
+ email: 'test@example.com',
64
+ password: '$2a$12$abcdefghijklmnopqrstuvwxyz',
65
+ role: 'user',
66
+ };
67
+
68
+ const cleanUser = {
69
+ _id: '123',
70
+ email: 'test@example.com',
71
+ firstName: 'John',
72
+ lastName: 'Doe',
73
+ role: 'user',
74
+ };
75
+
76
+ vi.mocked(userRepository.findByEmail).mockResolvedValueOnce(user);
77
+ vi.mocked(bcryptjs.compare).mockResolvedValueOnce(true);
78
+ vi.mocked(userRepository.findById).mockResolvedValueOnce(cleanUser);
79
+
80
+ const response = await request(app)
81
+ .post('/api/v1/users/login')
82
+ .send({
83
+ email: 'test@example.com',
84
+ password: 'Password123!',
85
+ });
86
+
87
+ expect(response.status).toBe(200);
88
+ expect(response.body).toHaveProperty('success', true);
89
+ expect(response.body.data).toHaveProperty('user');
90
+ expect(response.body.data.user).toHaveProperty('role', 'user');
91
+ });
92
+ });
93
+
94
+ describe('GET /health', () => {
95
+ it('should return health status', async () => {
96
+ const response = await request(app).get('/health');
97
+
98
+ expect(response.status).toBe(200);
99
+ expect(response.body).toHaveProperty('status', 'UP');
100
+ });
101
+ });
102
+
103
+ describe('404 handler', () => {
104
+ it('should return 404 for unknown routes', async () => {
105
+ const response = await request(app).get('/api/v1/unknown');
106
+
107
+ expect(response.status).toBe(404);
108
+ expect(response.body).toHaveProperty('success', false);
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,96 @@
1
+ // tests/unit/user.service.test.js — Unit tests for business logic. Mocked repository.
2
+
3
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
4
+ import * as userService from '../../src/modules/user/user.service.js';
5
+ import * as userRepository from '../../src/modules/user/user.repository.js';
6
+ import { AppError } from '../../src/utils/AppError.js';
7
+ import bcryptjs from 'bcryptjs';
8
+
9
+ vi.mock('../../src/modules/user/user.repository.js');
10
+ vi.mock('bcryptjs');
11
+
12
+ describe('User Service', () => {
13
+ beforeEach(() => {
14
+ vi.clearAllMocks();
15
+ });
16
+
17
+ describe('register', () => {
18
+ it('should create a new user successfully', async () => {
19
+ const userData = {
20
+ email: 'test@example.com',
21
+ firstName: 'John',
22
+ lastName: 'Doe',
23
+ password: 'hashedPassword123',
24
+ };
25
+
26
+ vi.mocked(userRepository.findByEmailPublic).mockResolvedValueOnce(null);
27
+ vi.mocked(bcryptjs.hash).mockResolvedValueOnce('hashedPassword123');
28
+ vi.mocked(userRepository.create).mockResolvedValueOnce(userData);
29
+
30
+ const result = await userService.register(
31
+ 'test@example.com',
32
+ 'John',
33
+ 'Doe',
34
+ 'password123'
35
+ );
36
+
37
+ expect(result).toEqual(userData);
38
+ expect(userRepository.findByEmailPublic).toHaveBeenCalledWith('test@example.com');
39
+ expect(bcryptjs.hash).toHaveBeenCalledWith('password123', expect.any(Number));
40
+ });
41
+
42
+ it('should throw error if email exists', async () => {
43
+ vi.mocked(userRepository.findByEmailPublic).mockResolvedValueOnce({ email: 'test@example.com' });
44
+
45
+ await expect(
46
+ userService.register('test@example.com', 'John', 'Doe', 'password123')
47
+ ).rejects.toThrow(AppError);
48
+ });
49
+ });
50
+
51
+ describe('login', () => {
52
+ it('should return user and tokens on successful login', async () => {
53
+ const user = {
54
+ _id: '123',
55
+ email: 'test@example.com',
56
+ password: 'hashedPassword123',
57
+ toObject: vi.fn().mockReturnValue({ _id: '123', email: 'test@example.com' }),
58
+ };
59
+
60
+ vi.mocked(userRepository.findByEmail).mockResolvedValueOnce(user);
61
+ vi.mocked(bcryptjs.compare).mockResolvedValueOnce(true);
62
+
63
+ const result = await userService.login('test@example.com', 'password123');
64
+
65
+ expect(result).toHaveProperty('user');
66
+ expect(result).toHaveProperty('token');
67
+ expect(result).toHaveProperty('refreshToken');
68
+ });
69
+
70
+ it('should throw error on invalid credentials', async () => {
71
+ vi.mocked(userRepository.findByEmail).mockResolvedValueOnce(null);
72
+
73
+ await expect(
74
+ userService.login('test@example.com', 'password123')
75
+ ).rejects.toThrow(AppError);
76
+ });
77
+ });
78
+
79
+ describe('getUserById', () => {
80
+ it('should return user by id', async () => {
81
+ const user = { _id: '123', email: 'test@example.com' };
82
+ vi.mocked(userRepository.findById).mockResolvedValueOnce(user);
83
+
84
+ const result = await userService.getUserById('123');
85
+
86
+ expect(result).toEqual(user);
87
+ expect(userRepository.findById).toHaveBeenCalledWith('123');
88
+ });
89
+
90
+ it('should throw error if user not found', async () => {
91
+ vi.mocked(userRepository.findById).mockResolvedValueOnce(null);
92
+
93
+ await expect(userService.getUserById('123')).rejects.toThrow(AppError);
94
+ });
95
+ });
96
+ });
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": ["eslint:recommended"],
3
+ "env": {
4
+ "node": true,
5
+ "es2022": true
6
+ },
7
+ "parserOptions": {
8
+ "ecmaVersion": 2022,
9
+ "sourceType": "module"
10
+ },
11
+ "rules": {
12
+ "no-console": "off",
13
+ "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
14
+ "prefer-const": "error",
15
+ "no-var": "error",
16
+ "eqeqeq": ["error", "always"],
17
+ "curly": "error",
18
+ "brace-style": ["error", "1tbs"],
19
+ "quotes": ["error", "single", { "avoidEscape": true }],
20
+ "semi": ["error", "always"],
21
+ "comma-dangle": ["error", "always-multiline"],
22
+ "indent": ["error", 2]
23
+ }
24
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5",
5
+ "printWidth": 100,
6
+ "tabWidth": 2,
7
+ "useTabs": false,
8
+ "arrowParens": "always"
9
+ }
@@ -0,0 +1,7 @@
1
+ NODE_ENV=development
2
+ PORT=3000
3
+ DATABASE_URL=postgresql://user:password@localhost:5432/theta_app
4
+ JWT_SECRET=your-super-secret-jwt-key-min-32-characters-required
5
+ JWT_REFRESH_SECRET=your-super-secret-refresh-key-min-32-characters-required
6
+ CORS_ORIGIN=http://localhost:3000
7
+ LOG_LEVEL=info
@@ -0,0 +1,20 @@
1
+ node_modules/
2
+ .env
3
+ .env.local
4
+ .env.*.local
5
+ dist/
6
+ build/
7
+ *.log
8
+ npm-debug.log*
9
+ yarn-debug.log*
10
+ yarn-error.log*
11
+ .DS_Store
12
+ .vscode/
13
+ .idea/
14
+ *.swp
15
+ *.swo
16
+ *~
17
+ .cache/
18
+ .env.test
19
+ coverage/
20
+ prisma/migrations/
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "my-awesome-app",
3
+ "version": "1.0.0",
4
+ "description": "Production-grade Node.js API built with create-theta-code",
5
+ "type": "module",
6
+ "main": "server.js",
7
+ "scripts": {
8
+ "start": "node server.js",
9
+ "dev": "nodemon server.js",
10
+ "test": "vitest",
11
+ "test:coverage": "vitest --coverage",
12
+ "lint": "eslint src/ --fix",
13
+ "format": "prettier --write \"src/**/*.js\"",
14
+ "db:migrate": "prisma migrate dev",
15
+ "db:generate": "prisma generate",
16
+ "db:reset": "prisma migrate reset"
17
+ },
18
+ "keywords": [
19
+ "nodejs",
20
+ "express",
21
+ "postgresql",
22
+ "prisma",
23
+ "api"
24
+ ],
25
+ "author": "Your Name",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "express": "^4.18.2",
29
+ "@prisma/client": "^5.7.0",
30
+ "bcryptjs": "^2.4.3",
31
+ "jsonwebtoken": "^9.1.0",
32
+ "dotenv": "^16.3.1",
33
+ "cors": "^2.8.5",
34
+ "helmet": "^7.1.0",
35
+ "express-rate-limit": "^7.1.5",
36
+ "morgan": "^1.10.0",
37
+ "zod": "^3.22.4"
38
+ },
39
+ "devDependencies": {
40
+ "nodemon": "^3.0.1",
41
+ "eslint": "^8.54.0",
42
+ "prettier": "^3.1.0",
43
+ "vitest": "^0.34.6",
44
+ "supertest": "^6.3.3",
45
+ "prisma": "^5.7.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18.0.0"
49
+ }
50
+ }
@@ -0,0 +1,23 @@
1
+ // prisma/schema.prisma — Prisma database schema. PostgreSQL with User model.
2
+
3
+ generator client {
4
+ provider = "prisma-client-js"
5
+ }
6
+
7
+ datasource db {
8
+ provider = "postgresql"
9
+ url = env("DATABASE_URL")
10
+ }
11
+
12
+ model User {
13
+ id Int @id @default(autoincrement())
14
+ email String @unique
15
+ firstName String
16
+ lastName String
17
+ password String
18
+ isActive Boolean @default(true)
19
+ createdAt DateTime @default(now())
20
+ updatedAt DateTime @updatedAt
21
+
22
+ @@map("users")
23
+ }
@@ -0,0 +1,63 @@
1
+ // server.js — HTTP server startup and graceful shutdown. Zero other logic.
2
+
3
+ import { createApp } from './src/config/app.config.js';
4
+ import { connectDB, disconnectDB } from './src/config/db.config.js';
5
+ import envConfig from './src/config/env.config.js';
6
+
7
+ let server;
8
+
9
+ async function startServer() {
10
+ try {
11
+ // Connect to database
12
+ await connectDB();
13
+
14
+ // Create Express app
15
+ const app = createApp();
16
+
17
+ // Start HTTP server
18
+ server = app.listen(envConfig.PORT, () => {
19
+ console.log(`✅ Server running on port ${envConfig.PORT}`);
20
+ });
21
+ } catch (err) {
22
+ console.error('❌ Failed to start server:', err.message);
23
+ process.exit(1);
24
+ }
25
+ }
26
+
27
+ // Graceful shutdown
28
+ process.on('SIGTERM', async () => {
29
+ console.log('📌 SIGTERM received, shutting down gracefully');
30
+ if (server) {
31
+ server.close(async () => {
32
+ console.log('✅ Server closed');
33
+ await disconnectDB();
34
+ process.exit(0);
35
+ });
36
+ }
37
+ });
38
+
39
+ process.on('SIGINT', async () => {
40
+ console.log('📌 SIGINT received, shutting down gracefully');
41
+ if (server) {
42
+ server.close(async () => {
43
+ console.log('✅ Server closed');
44
+ await disconnectDB();
45
+ process.exit(0);
46
+ });
47
+ }
48
+ });
49
+
50
+ // Unhandled promise rejections
51
+ process.on('unhandledRejection', (reason, promise) => {
52
+ console.error('❌ Unhandled Rejection at:', promise, 'reason:', reason);
53
+ process.exit(1);
54
+ });
55
+
56
+ // Uncaught exceptions
57
+ process.on('uncaughtException', (err) => {
58
+ console.error('❌ Uncaught Exception:', err);
59
+ process.exit(1);
60
+ });
61
+
62
+ // Start server
63
+ startServer();
@@ -0,0 +1,48 @@
1
+ // src/config/app.config.js — Express app factory. Middleware registration. Middleware order sacred.
2
+
3
+ import express from 'express';
4
+ import helmet from 'helmet';
5
+ import cors from 'cors';
6
+ import morgan from 'morgan';
7
+ import envConfig from './env.config.js';
8
+ import { globalRateLimiter } from './rateLimiter.config.js';
9
+ import { notFoundMiddleware } from '../middlewares/notFound.middleware.js';
10
+ import { errorMiddleware } from '../middlewares/error.middleware.js';
11
+ import { APP_CONSTANTS } from '../utils/constants.js';
12
+ import userRoutes from '../modules/user/user.routes.js';
13
+
14
+ function createApp() {
15
+ const app = express();
16
+
17
+ // Security middleware
18
+ app.use(helmet());
19
+ app.use(cors({ origin: envConfig.CORS_ORIGIN }));
20
+
21
+ // Body parser with 10kb limit
22
+ app.use(express.json({ limit: APP_CONSTANTS.REQUEST_BODY_LIMIT }));
23
+ app.use(express.urlencoded({ limit: APP_CONSTANTS.REQUEST_BODY_LIMIT }));
24
+
25
+ // Logging
26
+ app.use(morgan('combined'));
27
+
28
+ // Rate limiter on all /api routes
29
+ app.use('/api/', globalRateLimiter);
30
+
31
+ // Health check
32
+ app.get('/health', (req, res) => {
33
+ res.json({ status: 'UP', timestamp: new Date().toISOString() });
34
+ });
35
+
36
+ // API routes
37
+ app.use('/api/v1/users', userRoutes);
38
+
39
+ // 404 handler
40
+ app.use(notFoundMiddleware);
41
+
42
+ // Global error handler (must be last)
43
+ app.use(errorMiddleware);
44
+
45
+ return app;
46
+ }
47
+
48
+ export { createApp };