Skip to content

Commit

Permalink
Merge pull request #29 from edwinhern/feat/SwaggerUI
Browse files Browse the repository at this point in the history
Feat/swagger UI
  • Loading branch information
edwinhern authored Jan 23, 2024
2 parents c33f644 + 01bc2a0 commit 0a87951
Show file tree
Hide file tree
Showing 12 changed files with 213 additions and 27 deletions.
4 changes: 2 additions & 2 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# App's running environment
NODE_ENV=development
NODE_ENV="development"

# App's running port
PORT=8080
PORT="8080"

# Cors Origin URL
CORS_ORIGIN=""
Expand Down
1 change: 0 additions & 1 deletion local.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ WORKDIR /app
COPY package*.json ./

# Install global and app dependencies
RUN npm install -g typescript tsx
RUN npm install

# Bundle app source
Expand Down
58 changes: 51 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@
"docker:stop": "docker-compose down"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^6.3.1",
"cors": "^2.8.5",
"crypto": "^1.0.1",
"dotenv": "^16.3.2",
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"helmet": "^7.1.0",
"http-status-codes": "^2.3.0",
"lodash": "^4.17.21",
"pino-http": "^9.0.0",
"swagger-ui-express": "^5.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
Expand All @@ -36,6 +37,7 @@
"@types/jest": "^29.5.11",
"@types/lodash": "^4.14.202",
"@types/supertest": "^6.0.2",
"@types/swagger-ui-express": "^4.1.6",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.56.0",
Expand Down
17 changes: 17 additions & 0 deletions src/common/middleware/openAPIDocument.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { OpenApiGeneratorV3, OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';

import { healthCheckRegistry } from '@modules/healthCheck/healthCheckRegistery';
import { userRegistry } from '@modules/user/userRegistery';

export function generateOpenAPIDocument() {
const registry = new OpenAPIRegistry([healthCheckRegistry, userRegistry]);
const generator = new OpenApiGeneratorV3(registry.definitions);

return generator.generateDocument({
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Swagger API',
},
});
}
14 changes: 11 additions & 3 deletions src/common/models/serviceResponse.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import _ from 'lodash';
import { z } from 'zod';

export enum ResponseStatus {
Success,
Expand All @@ -11,10 +11,18 @@ export class ServiceResponse<T = null> {
responseObject: T;
statusCode: number;

constructor(success: ResponseStatus, message: string, responseObject: T, statusCode: number) {
this.success = _.isEqual(success, ResponseStatus.Success);
constructor(status: ResponseStatus, message: string, responseObject: T, statusCode: number) {
this.success = status === ResponseStatus.Success;
this.message = message;
this.responseObject = responseObject;
this.statusCode = statusCode;
}
}

export const ServiceResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
success: z.boolean(),
message: z.string(),
responseObject: dataSchema.optional(),
statusCode: z.number(),
});
6 changes: 5 additions & 1 deletion src/common/utils/commonValidation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { z } from 'zod';

export const commonValidations = {
id: z.string().regex(/^\d+$/, 'ID must be a number').transform(Number),
id: z
.string()
.refine((data) => !isNaN(Number(data)), 'ID must be a numeric value')
.transform(Number)
.refine((num) => num > 0, 'ID must be a positive number'),
// ... other common validations
};
2 changes: 1 addition & 1 deletion src/common/utils/httpHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const validateRequest = (schema: ZodSchema) => (req: Request, res: Respon
schema.parse({ body: req.body, query: req.query, params: req.params });
next();
} catch (err) {
const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(' ')}`;
const errorMessage = `Invalid input: ${(err as ZodError).errors.map((e) => e.message).join(', ')}`;
const statusCode = StatusCodes.BAD_REQUEST;
res.status(statusCode).send(new ServiceResponse<null>(ResponseStatus.Failed, errorMessage, null, statusCode));
}
Expand Down
30 changes: 30 additions & 0 deletions src/modules/healthCheck/healthCheckRegistery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { StatusCodes } from 'http-status-codes';
import { z } from 'zod';

import { ServiceResponseSchema } from '@common/models/serviceResponse';

const HealthCheckResponseSchema = ServiceResponseSchema(z.null());
const healthCheckRegistry = new OpenAPIRegistry();

// Define and register health check API Route
healthCheckRegistry.registerPath({
method: 'get',
path: '/health-check',
tags: ['Health Check'],
description: 'Health check endpoint',
summary: 'Health Check',
operationId: 'getHealthCheck',
responses: {
[StatusCodes.OK]: {
description: 'Service is healthy',
content: {
'application/json': {
schema: HealthCheckResponseSchema,
},
},
},
},
});

export { healthCheckRegistry };
29 changes: 18 additions & 11 deletions src/modules/user/userModel.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
import { extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

import { commonValidations } from '@common/utils/commonValidation';

extendZodWithOpenApi(z);

export type User = z.infer<typeof UserSchema>;
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number(),
createdAt: z.date(),
updatedAt: z.date(),
});
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
age: z.number(),
createdAt: z.date(),
updatedAt: z.date(),
})
.openapi('User');

// Schema for 'GET users/:id' endpoint
export const GetUserSchema = z.object({
params: z.object({ id: commonValidations.id }),
});
export const GetUserSchema = z
.object({
params: z.object({ id: commonValidations.id }),
})
.openapi('GetUser');
69 changes: 69 additions & 0 deletions src/modules/user/userRegistery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { OpenAPIRegistry } from '@asteasolutions/zod-to-openapi';
import { StatusCodes } from 'http-status-codes';
import { z } from 'zod';

import { ServiceResponseSchema } from '@common/models/serviceResponse';
import { GetUserSchema, UserSchema } from '@modules/user/userModel';

const userRegistry = new OpenAPIRegistry();

// Register schemas
userRegistry.register('User', UserSchema);
userRegistry.register('GetUser', GetUserSchema);

// Define and register user API Routes
userRegistry.registerPath({
method: 'get',
path: '/users',
tags: ['User'],
description: 'Retrieve all users',
summary: 'Get Users',
responses: {
[StatusCodes.OK]: {
description: 'List of users',
content: {
'application/json': {
schema: ServiceResponseSchema(z.array(UserSchema)),
},
},
},
[StatusCodes.NOT_FOUND]: {
description: 'User not found',
},
[StatusCodes.INTERNAL_SERVER_ERROR]: {
description: 'Internal server error',
},
},
});

userRegistry.registerPath({
method: 'get',
path: '/users/{id}',
tags: ['User'],
description: 'Retrieve a single user by ID',
summary: 'Get User by ID',
request: {
params: GetUserSchema.shape.params,
},
responses: {
[StatusCodes.OK]: {
description: 'User data',
content: {
'application/json': {
schema: ServiceResponseSchema(UserSchema),
},
},
},
[StatusCodes.NOT_FOUND]: {
description: 'User not found',
},
[StatusCodes.INTERNAL_SERVER_ERROR]: {
description: 'Internal server error',
},
[StatusCodes.BAD_REQUEST]: {
description: 'Bad request',
},
},
});

export { userRegistry };
6 changes: 6 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import express, { Express } from 'express';
import helmet from 'helmet';
import path from 'path';
import { pino } from 'pino';
import swaggerUi from 'swagger-ui-express';

import errorHandler from '@common/middleware/errorHandler';
import { generateOpenAPIDocument } from '@common/middleware/openAPIDocument';
import rateLimiter from '@common/middleware/rateLimiter';
import requestLogger from '@common/middleware/requestLogger';
import { getCorsOrigin } from '@common/utils/envConfig';
Expand All @@ -32,6 +34,10 @@ app.use(requestLogger());
app.use('/health-check', healthCheckRouter);
app.use('/users', userRouter);

// Swagger UI
const openAPIDocument = generateOpenAPIDocument();
app.use('/', swaggerUi.serve, swaggerUi.setup(openAPIDocument));

// Error handlers
app.use(errorHandler());

Expand Down

0 comments on commit 0a87951

Please sign in to comment.