create-listablelabs-api 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 (32) hide show
  1. package/README.md +142 -0
  2. package/bin/cli.js +37 -0
  3. package/bin/commands/add.js +460 -0
  4. package/bin/commands/create.js +481 -0
  5. package/package.json +39 -0
  6. package/templates/base/.dockerignore +19 -0
  7. package/templates/base/.env.example +18 -0
  8. package/templates/base/.eslintrc.js +31 -0
  9. package/templates/base/Dockerfile +48 -0
  10. package/templates/base/README.md +295 -0
  11. package/templates/base/docker-compose.yml +55 -0
  12. package/templates/base/jest.config.js +24 -0
  13. package/templates/base/package.json +41 -0
  14. package/templates/base/src/app.js +103 -0
  15. package/templates/base/src/config/index.js +36 -0
  16. package/templates/base/src/controllers/exampleController.js +148 -0
  17. package/templates/base/src/database/baseModel.js +160 -0
  18. package/templates/base/src/database/index.js +108 -0
  19. package/templates/base/src/middlewares/errorHandler.js +155 -0
  20. package/templates/base/src/middlewares/index.js +49 -0
  21. package/templates/base/src/middlewares/rateLimiter.js +85 -0
  22. package/templates/base/src/middlewares/requestLogger.js +50 -0
  23. package/templates/base/src/middlewares/validator.js +107 -0
  24. package/templates/base/src/models/example.js +117 -0
  25. package/templates/base/src/models/index.js +6 -0
  26. package/templates/base/src/routes/v1/exampleRoutes.js +89 -0
  27. package/templates/base/src/routes/v1/index.js +19 -0
  28. package/templates/base/src/server.js +80 -0
  29. package/templates/base/src/utils/logger.js +61 -0
  30. package/templates/base/src/utils/response.js +117 -0
  31. package/templates/base/tests/app.test.js +215 -0
  32. package/templates/base/tests/setup.js +33 -0
@@ -0,0 +1,117 @@
1
+ const { createModel } = require('../database/baseModel');
2
+ const { z } = require('zod');
3
+
4
+ /**
5
+ * Zod schema for validation
6
+ * Use this in routes for request validation
7
+ */
8
+ const exampleZodSchema = {
9
+ create: z.object({
10
+ name: z.string().min(1).max(255),
11
+ description: z.string().max(1000).optional(),
12
+ status: z.enum(['active', 'inactive', 'pending']).default('pending'),
13
+ tags: z.array(z.string()).max(10).optional(),
14
+ metadata: z.record(z.any()).optional(),
15
+ }),
16
+
17
+ update: z.object({
18
+ name: z.string().min(1).max(255).optional(),
19
+ description: z.string().max(1000).optional(),
20
+ status: z.enum(['active', 'inactive', 'pending']).optional(),
21
+ tags: z.array(z.string()).max(10).optional(),
22
+ metadata: z.record(z.any()).optional(),
23
+ }),
24
+
25
+ query: z.object({
26
+ page: z.coerce.number().int().min(1).default(1),
27
+ limit: z.coerce.number().int().min(1).max(100).default(20),
28
+ status: z.enum(['active', 'inactive', 'pending']).optional(),
29
+ search: z.string().optional(),
30
+ sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
31
+ sortOrder: z.enum(['asc', 'desc']).default('desc'),
32
+ }),
33
+ };
34
+
35
+ /**
36
+ * Mongoose model
37
+ */
38
+ const Example = createModel(
39
+ 'Example',
40
+ {
41
+ name: {
42
+ type: String,
43
+ required: true,
44
+ trim: true,
45
+ maxlength: 255,
46
+ },
47
+ description: {
48
+ type: String,
49
+ trim: true,
50
+ maxlength: 1000,
51
+ },
52
+ status: {
53
+ type: String,
54
+ enum: ['active', 'inactive', 'pending'],
55
+ default: 'pending',
56
+ },
57
+ tags: {
58
+ type: [String],
59
+ default: [],
60
+ },
61
+ metadata: {
62
+ type: Map,
63
+ of: String,
64
+ },
65
+ },
66
+ {
67
+ // Enable soft delete
68
+ softDelete: true,
69
+
70
+ // Add indexes
71
+ indexes: [
72
+ { fields: { name: 'text', description: 'text' } },
73
+ { fields: { status: 1 } },
74
+ { fields: { tags: 1 } },
75
+ { fields: { createdAt: -1 } },
76
+ ],
77
+
78
+ // Custom static methods
79
+ statics: {
80
+ async findByStatus(status) {
81
+ return this.find({ status });
82
+ },
83
+
84
+ async search(query, options = {}) {
85
+ const filter = {};
86
+
87
+ if (query.search) {
88
+ filter.$text = { $search: query.search };
89
+ }
90
+
91
+ if (query.status) {
92
+ filter.status = query.status;
93
+ }
94
+
95
+ return this.paginate(filter, options);
96
+ },
97
+ },
98
+
99
+ // Custom instance methods
100
+ methods: {
101
+ activate() {
102
+ this.status = 'active';
103
+ return this.save();
104
+ },
105
+
106
+ deactivate() {
107
+ this.status = 'inactive';
108
+ return this.save();
109
+ },
110
+ },
111
+ }
112
+ );
113
+
114
+ module.exports = {
115
+ Example,
116
+ exampleZodSchema,
117
+ };
@@ -0,0 +1,6 @@
1
+ const { Example, exampleZodSchema } = require('./example');
2
+
3
+ module.exports = {
4
+ Example,
5
+ exampleZodSchema,
6
+ };
@@ -0,0 +1,89 @@
1
+ const { Router } = require('express');
2
+ const exampleController = require('../../controllers/exampleController');
3
+ const { validate, z } = require('../../middlewares');
4
+ const { exampleZodSchema } = require('../../models');
5
+
6
+ const router = Router();
7
+
8
+ /**
9
+ * Validation schemas
10
+ */
11
+ const schemas = {
12
+ getById: {
13
+ params: z.object({
14
+ id: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid ID format'),
15
+ }),
16
+ },
17
+
18
+ getAll: {
19
+ query: exampleZodSchema.query,
20
+ },
21
+
22
+ create: {
23
+ body: exampleZodSchema.create,
24
+ },
25
+
26
+ update: {
27
+ params: z.object({
28
+ id: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid ID format'),
29
+ }),
30
+ body: exampleZodSchema.update,
31
+ },
32
+
33
+ delete: {
34
+ params: z.object({
35
+ id: z.string().regex(/^[0-9a-fA-F]{24}$/, 'Invalid ID format'),
36
+ }),
37
+ },
38
+ };
39
+
40
+ /**
41
+ * @route GET /api/v1/examples
42
+ * @desc Get all examples with pagination
43
+ * @access Public
44
+ */
45
+ router.get('/', validate(schemas.getAll), exampleController.getAll);
46
+
47
+ /**
48
+ * @route GET /api/v1/examples/:id
49
+ * @desc Get example by ID
50
+ * @access Public
51
+ */
52
+ router.get('/:id', validate(schemas.getById), exampleController.getById);
53
+
54
+ /**
55
+ * @route POST /api/v1/examples
56
+ * @desc Create new example
57
+ * @access Public
58
+ */
59
+ router.post('/', validate(schemas.create), exampleController.create);
60
+
61
+ /**
62
+ * @route PUT /api/v1/examples/:id
63
+ * @desc Update example
64
+ * @access Public
65
+ */
66
+ router.put('/:id', validate(schemas.update), exampleController.update);
67
+
68
+ /**
69
+ * @route DELETE /api/v1/examples/:id
70
+ * @desc Delete example (soft delete)
71
+ * @access Public
72
+ */
73
+ router.delete('/:id', validate(schemas.delete), exampleController.remove);
74
+
75
+ /**
76
+ * @route PATCH /api/v1/examples/:id/activate
77
+ * @desc Activate example
78
+ * @access Public
79
+ */
80
+ router.patch('/:id/activate', validate(schemas.getById), exampleController.activate);
81
+
82
+ /**
83
+ * @route PATCH /api/v1/examples/:id/deactivate
84
+ * @desc Deactivate example
85
+ * @access Public
86
+ */
87
+ router.patch('/:id/deactivate', validate(schemas.getById), exampleController.deactivate);
88
+
89
+ module.exports = router;
@@ -0,0 +1,19 @@
1
+ const { Router } = require('express');
2
+ const exampleRoutes = require('./exampleRoutes');
3
+
4
+ const router = Router();
5
+
6
+ /**
7
+ * Register all route modules here
8
+ * Each module handles its own sub-routes
9
+ */
10
+
11
+ // Example routes - replace with your actual routes
12
+ router.use('/examples', exampleRoutes);
13
+
14
+ // Add more route modules as needed:
15
+ // router.use('/users', userRoutes);
16
+ // router.use('/products', productRoutes);
17
+ // router.use('/orders', orderRoutes);
18
+
19
+ module.exports = router;
@@ -0,0 +1,80 @@
1
+ const app = require('./app');
2
+ const config = require('./config');
3
+ const { logger } = require('./utils/logger');
4
+ const { connectDB, disconnectDB } = require('./database');
5
+
6
+ /**
7
+ * Start the server
8
+ */
9
+ const startServer = async () => {
10
+ try {
11
+ // Connect to MongoDB Atlas
12
+ await connectDB();
13
+
14
+ // Start Express server
15
+ const server = app.listen(config.port, () => {
16
+ logger.info({
17
+ port: config.port,
18
+ env: config.env,
19
+ service: config.serviceName,
20
+ }, `🚀 ${config.serviceName} is running on port ${config.port}`);
21
+ });
22
+
23
+ /**
24
+ * Graceful shutdown handling
25
+ */
26
+ const gracefulShutdown = async (signal) => {
27
+ logger.info({ signal }, 'Received shutdown signal, starting graceful shutdown...');
28
+
29
+ // Stop accepting new requests
30
+ server.close(async (err) => {
31
+ if (err) {
32
+ logger.error({ err }, 'Error during server close');
33
+ process.exit(1);
34
+ }
35
+
36
+ logger.info('Server closed successfully');
37
+
38
+ // Close database connection
39
+ await disconnectDB();
40
+
41
+ logger.info('All connections closed, exiting process');
42
+ process.exit(0);
43
+ });
44
+
45
+ // Force shutdown after timeout
46
+ setTimeout(() => {
47
+ logger.error('Forced shutdown due to timeout');
48
+ process.exit(1);
49
+ }, 30000); // 30 seconds timeout
50
+ };
51
+
52
+ // Handle shutdown signals
53
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
54
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
55
+
56
+ return server;
57
+ } catch (err) {
58
+ logger.fatal({ err }, 'Failed to start server');
59
+ process.exit(1);
60
+ }
61
+ };
62
+
63
+ /**
64
+ * Handle uncaught exceptions
65
+ */
66
+ process.on('uncaughtException', (err) => {
67
+ logger.fatal({ err }, 'Uncaught exception');
68
+ process.exit(1);
69
+ });
70
+
71
+ /**
72
+ * Handle unhandled promise rejections
73
+ */
74
+ process.on('unhandledRejection', (reason, promise) => {
75
+ logger.fatal({ reason, promise }, 'Unhandled promise rejection');
76
+ process.exit(1);
77
+ });
78
+
79
+ // Start the server
80
+ startServer();
@@ -0,0 +1,61 @@
1
+ const pino = require('pino');
2
+ const config = require('../config');
3
+
4
+ // Base logger instance
5
+ const logger = pino({
6
+ name: config.serviceName,
7
+ level: config.logLevel,
8
+
9
+ // Custom serializers for consistent log format
10
+ serializers: {
11
+ req: (req) => ({
12
+ method: req.method,
13
+ url: req.url,
14
+ path: req.path,
15
+ params: req.params,
16
+ query: req.query,
17
+ headers: {
18
+ 'user-agent': req.headers['user-agent'],
19
+ 'content-type': req.headers['content-type'],
20
+ 'x-request-id': req.headers['x-request-id'],
21
+ },
22
+ }),
23
+ res: (res) => ({
24
+ statusCode: res.statusCode,
25
+ }),
26
+ err: pino.stdSerializers.err,
27
+ },
28
+
29
+ // Add base context to all logs
30
+ base: {
31
+ service: config.serviceName,
32
+ env: config.env,
33
+ pid: process.pid,
34
+ },
35
+
36
+ // Timestamp format
37
+ timestamp: pino.stdTimeFunctions.isoTime,
38
+
39
+ // Pretty print in development
40
+ transport:
41
+ config.env === 'development'
42
+ ? {
43
+ target: 'pino-pretty',
44
+ options: {
45
+ colorize: true,
46
+ translateTime: 'SYS:standard',
47
+ ignore: 'pid,hostname',
48
+ },
49
+ }
50
+ : undefined,
51
+ });
52
+
53
+ // Create child logger with additional context
54
+ const createChildLogger = (context) => {
55
+ return logger.child(context);
56
+ };
57
+
58
+ module.exports = {
59
+ logger,
60
+ createChildLogger,
61
+ };
@@ -0,0 +1,117 @@
1
+ const { StatusCodes } = require('http-status-codes');
2
+
3
+ /**
4
+ * Standard API response formatter
5
+ * Ensures all responses follow the same structure
6
+ */
7
+
8
+ /**
9
+ * Send success response
10
+ * @param {Object} res - Express response object
11
+ * @param {Object} data - Response data
12
+ * @param {number} statusCode - HTTP status code (default: 200)
13
+ * @param {string} message - Optional success message
14
+ */
15
+ const sendSuccess = (res, data = null, statusCode = StatusCodes.OK, message = null) => {
16
+ const response = {
17
+ success: true,
18
+ ...(message && { message }),
19
+ ...(data !== null && { data }),
20
+ timestamp: new Date().toISOString(),
21
+ };
22
+
23
+ return res.status(statusCode).json(response);
24
+ };
25
+
26
+ /**
27
+ * Send created response (201)
28
+ * @param {Object} res - Express response object
29
+ * @param {Object} data - Created resource data
30
+ * @param {string} message - Optional message
31
+ */
32
+ const sendCreated = (res, data, message = 'Resource created successfully') => {
33
+ return sendSuccess(res, data, StatusCodes.CREATED, message);
34
+ };
35
+
36
+ /**
37
+ * Send no content response (204)
38
+ * @param {Object} res - Express response object
39
+ */
40
+ const sendNoContent = (res) => {
41
+ return res.status(StatusCodes.NO_CONTENT).send();
42
+ };
43
+
44
+ /**
45
+ * Send paginated response
46
+ * @param {Object} res - Express response object
47
+ * @param {Array} data - Array of items
48
+ * @param {Object} pagination - Pagination info
49
+ * @param {number} pagination.page - Current page
50
+ * @param {number} pagination.limit - Items per page
51
+ * @param {number} pagination.total - Total items
52
+ */
53
+ const sendPaginated = (res, data, { page, limit, total }) => {
54
+ const totalPages = Math.ceil(total / limit);
55
+
56
+ const response = {
57
+ success: true,
58
+ data,
59
+ pagination: {
60
+ page,
61
+ limit,
62
+ total,
63
+ totalPages,
64
+ hasNextPage: page < totalPages,
65
+ hasPrevPage: page > 1,
66
+ },
67
+ timestamp: new Date().toISOString(),
68
+ };
69
+
70
+ return res.status(StatusCodes.OK).json(response);
71
+ };
72
+
73
+ /**
74
+ * Response builder for complex responses
75
+ * Usage: responseBuilder(res).data(users).message('Users fetched').status(200).send()
76
+ */
77
+ const responseBuilder = (res) => {
78
+ const response = {
79
+ success: true,
80
+ timestamp: new Date().toISOString(),
81
+ };
82
+ let statusCode = StatusCodes.OK;
83
+
84
+ return {
85
+ data(data) {
86
+ response.data = data;
87
+ return this;
88
+ },
89
+ message(msg) {
90
+ response.message = msg;
91
+ return this;
92
+ },
93
+ status(code) {
94
+ statusCode = code;
95
+ return this;
96
+ },
97
+ meta(meta) {
98
+ response.meta = meta;
99
+ return this;
100
+ },
101
+ pagination(paginationData) {
102
+ response.pagination = paginationData;
103
+ return this;
104
+ },
105
+ send() {
106
+ return res.status(statusCode).json(response);
107
+ },
108
+ };
109
+ };
110
+
111
+ module.exports = {
112
+ sendSuccess,
113
+ sendCreated,
114
+ sendNoContent,
115
+ sendPaginated,
116
+ responseBuilder,
117
+ };
@@ -0,0 +1,215 @@
1
+ const request = require('supertest');
2
+ const app = require('../src/app');
3
+ const { Example } = require('../src/models');
4
+
5
+ describe('Health Endpoints', () => {
6
+ describe('GET /health', () => {
7
+ it('should return healthy status', async () => {
8
+ const res = await request(app).get('/health');
9
+
10
+ expect(res.statusCode).toBe(200);
11
+ expect(res.body).toHaveProperty('status', 'healthy');
12
+ expect(res.body).toHaveProperty('service');
13
+ expect(res.body).toHaveProperty('timestamp');
14
+ expect(res.body).toHaveProperty('uptime');
15
+ });
16
+ });
17
+
18
+ describe('GET /ready', () => {
19
+ it('should return ready status when DB is connected', async () => {
20
+ const res = await request(app).get('/ready');
21
+
22
+ expect(res.statusCode).toBe(200);
23
+ expect(res.body).toHaveProperty('status', 'ready');
24
+ expect(res.body.checks).toHaveProperty('database');
25
+ });
26
+ });
27
+ });
28
+
29
+ describe('Example API Endpoints', () => {
30
+ describe('GET /api/v1/examples', () => {
31
+ it('should return empty array when no examples exist', async () => {
32
+ const res = await request(app).get('/api/v1/examples');
33
+
34
+ expect(res.statusCode).toBe(200);
35
+ expect(res.body).toHaveProperty('success', true);
36
+ expect(res.body.data).toEqual([]);
37
+ expect(res.body.pagination.total).toBe(0);
38
+ });
39
+
40
+ it('should return paginated examples', async () => {
41
+ // Create test data
42
+ await Example.create([
43
+ { name: 'Test 1' },
44
+ { name: 'Test 2' },
45
+ { name: 'Test 3' },
46
+ ]);
47
+
48
+ const res = await request(app)
49
+ .get('/api/v1/examples')
50
+ .query({ page: 1, limit: 2 });
51
+
52
+ expect(res.statusCode).toBe(200);
53
+ expect(res.body.data).toHaveLength(2);
54
+ expect(res.body.pagination.page).toBe(1);
55
+ expect(res.body.pagination.limit).toBe(2);
56
+ expect(res.body.pagination.total).toBe(3);
57
+ expect(res.body.pagination.totalPages).toBe(2);
58
+ });
59
+
60
+ it('should filter by status', async () => {
61
+ await Example.create([
62
+ { name: 'Active 1', status: 'active' },
63
+ { name: 'Active 2', status: 'active' },
64
+ { name: 'Inactive', status: 'inactive' },
65
+ ]);
66
+
67
+ const res = await request(app)
68
+ .get('/api/v1/examples')
69
+ .query({ status: 'active' });
70
+
71
+ expect(res.statusCode).toBe(200);
72
+ expect(res.body.data).toHaveLength(2);
73
+ expect(res.body.data.every((item) => item.status === 'active')).toBe(true);
74
+ });
75
+ });
76
+
77
+ describe('GET /api/v1/examples/:id', () => {
78
+ it('should return example by ID', async () => {
79
+ const example = await Example.create({ name: 'Test Example' });
80
+
81
+ const res = await request(app).get(`/api/v1/examples/${example._id}`);
82
+
83
+ expect(res.statusCode).toBe(200);
84
+ expect(res.body.success).toBe(true);
85
+ expect(res.body.data.name).toBe('Test Example');
86
+ });
87
+
88
+ it('should return 404 for non-existent ID', async () => {
89
+ const fakeId = '507f1f77bcf86cd799439011';
90
+ const res = await request(app).get(`/api/v1/examples/${fakeId}`);
91
+
92
+ expect(res.statusCode).toBe(404);
93
+ expect(res.body.success).toBe(false);
94
+ expect(res.body.error.code).toBe('NOT_FOUND');
95
+ });
96
+
97
+ it('should return 422 for invalid ID format', async () => {
98
+ const res = await request(app).get('/api/v1/examples/invalid-id');
99
+
100
+ expect(res.statusCode).toBe(422);
101
+ expect(res.body.success).toBe(false);
102
+ expect(res.body.error.code).toBe('VALIDATION_ERROR');
103
+ });
104
+ });
105
+
106
+ describe('POST /api/v1/examples', () => {
107
+ it('should create new example', async () => {
108
+ const res = await request(app)
109
+ .post('/api/v1/examples')
110
+ .send({ name: 'New Example', description: 'Test description' });
111
+
112
+ expect(res.statusCode).toBe(201);
113
+ expect(res.body.success).toBe(true);
114
+ expect(res.body.data.name).toBe('New Example');
115
+ expect(res.body.data.status).toBe('pending'); // default value
116
+
117
+ // Verify it's in the database
118
+ const example = await Example.findById(res.body.data.id);
119
+ expect(example).not.toBeNull();
120
+ });
121
+
122
+ it('should return validation error for missing name', async () => {
123
+ const res = await request(app)
124
+ .post('/api/v1/examples')
125
+ .send({ description: 'No name provided' });
126
+
127
+ expect(res.statusCode).toBe(422);
128
+ expect(res.body.success).toBe(false);
129
+ expect(res.body.error.code).toBe('VALIDATION_ERROR');
130
+ });
131
+
132
+ it('should validate status enum', async () => {
133
+ const res = await request(app)
134
+ .post('/api/v1/examples')
135
+ .send({ name: 'Test', status: 'invalid-status' });
136
+
137
+ expect(res.statusCode).toBe(422);
138
+ expect(res.body.error.code).toBe('VALIDATION_ERROR');
139
+ });
140
+ });
141
+
142
+ describe('PUT /api/v1/examples/:id', () => {
143
+ it('should update example', async () => {
144
+ const example = await Example.create({ name: 'Original' });
145
+
146
+ const res = await request(app)
147
+ .put(`/api/v1/examples/${example._id}`)
148
+ .send({ name: 'Updated' });
149
+
150
+ expect(res.statusCode).toBe(200);
151
+ expect(res.body.success).toBe(true);
152
+ expect(res.body.data.name).toBe('Updated');
153
+ });
154
+
155
+ it('should return 404 for non-existent ID', async () => {
156
+ const fakeId = '507f1f77bcf86cd799439011';
157
+ const res = await request(app)
158
+ .put(`/api/v1/examples/${fakeId}`)
159
+ .send({ name: 'Updated' });
160
+
161
+ expect(res.statusCode).toBe(404);
162
+ });
163
+ });
164
+
165
+ describe('DELETE /api/v1/examples/:id', () => {
166
+ it('should soft delete example', async () => {
167
+ const example = await Example.create({ name: 'To Delete' });
168
+
169
+ const res = await request(app).delete(`/api/v1/examples/${example._id}`);
170
+
171
+ expect(res.statusCode).toBe(204);
172
+
173
+ // Verify soft delete (not actually removed)
174
+ const deleted = await Example.findOne({
175
+ _id: example._id,
176
+ includeDeleted: true,
177
+ });
178
+ expect(deleted).not.toBeNull();
179
+ });
180
+ });
181
+
182
+ describe('PATCH /api/v1/examples/:id/activate', () => {
183
+ it('should activate example', async () => {
184
+ const example = await Example.create({ name: 'Test', status: 'pending' });
185
+
186
+ const res = await request(app).patch(`/api/v1/examples/${example._id}/activate`);
187
+
188
+ expect(res.statusCode).toBe(200);
189
+ expect(res.body.data.status).toBe('active');
190
+ });
191
+ });
192
+
193
+ describe('PATCH /api/v1/examples/:id/deactivate', () => {
194
+ it('should deactivate example', async () => {
195
+ const example = await Example.create({ name: 'Test', status: 'active' });
196
+
197
+ const res = await request(app).patch(`/api/v1/examples/${example._id}/deactivate`);
198
+
199
+ expect(res.statusCode).toBe(200);
200
+ expect(res.body.data.status).toBe('inactive');
201
+ });
202
+ });
203
+ });
204
+
205
+ describe('Error Handling', () => {
206
+ describe('404 Not Found', () => {
207
+ it('should return 404 for undefined routes', async () => {
208
+ const res = await request(app).get('/api/v1/nonexistent');
209
+
210
+ expect(res.statusCode).toBe(404);
211
+ expect(res.body.success).toBe(false);
212
+ expect(res.body.error.code).toBe('NOT_FOUND');
213
+ });
214
+ });
215
+ });