diff --git a/package.json b/package.json index e99696f..e22ecf2 100644 --- a/package.json +++ b/package.json @@ -2,10 +2,10 @@ "name": "express-typescript-boilerplate", "version": "1.0.14", "description": "An Express boilerplate backend", - "author": "Edwin Hernandez", - "repository": "edwinhern/express-typescript-2024", + "author": "Alejandro Ruiz", + "repository": "alex-ruiz/express-typescript-2024", "license": "MIT", - "main": "index.ts", + "main": "src/index.ts", "private": true, "scripts": { "dev": "tsx watch --clear-screen=false src/index.ts | pino-pretty", diff --git a/src/api-docs/__tests__/openAPIRouter.test.ts b/src/api-docs/__tests__/openAPIRouter.test.ts index 131b293..9b6dbc7 100644 --- a/src/api-docs/__tests__/openAPIRouter.test.ts +++ b/src/api-docs/__tests__/openAPIRouter.test.ts @@ -1,7 +1,7 @@ import { StatusCodes } from "http-status-codes"; import request from "supertest"; -import { app } from "@/server"; +import { app } from "@/bin/server"; import { generateOpenAPIDocument } from "../openAPIDocumentGenerator"; @@ -12,7 +12,7 @@ describe("OpenAPI Router", () => { const expectedResponse = generateOpenAPIDocument(); // Act - const response = await request(app).get("/swagger.json"); + const response = await request(app).get("/swagger/swagger.json"); // Assert expect(response.status).toBe(StatusCodes.OK); @@ -22,7 +22,7 @@ describe("OpenAPI Router", () => { it("should serve the Swagger UI", async () => { // Act - const response = await request(app).get("/"); + const response = await request(app).get("/swagger/"); // Assert expect(response.status).toBe(StatusCodes.OK); diff --git a/src/api-docs/openAPIDocumentGenerator.ts b/src/api-docs/openAPIDocumentGenerator.ts index 4c5abfb..727fda9 100644 --- a/src/api-docs/openAPIDocumentGenerator.ts +++ b/src/api-docs/openAPIDocumentGenerator.ts @@ -15,7 +15,7 @@ export function generateOpenAPIDocument() { }, externalDocs: { description: "View the raw OpenAPI Specification in JSON format", - url: "/swagger.json", + url: "/swagger/swagger.json", }, }); } diff --git a/src/api/healthCheck/__tests__/healthCheckRouter.test.ts b/src/api/healthCheck/__tests__/healthCheckRouter.test.ts index 791d65c..619033d 100644 --- a/src/api/healthCheck/__tests__/healthCheckRouter.test.ts +++ b/src/api/healthCheck/__tests__/healthCheckRouter.test.ts @@ -1,8 +1,8 @@ import { StatusCodes } from "http-status-codes"; import request from "supertest"; +import { app } from "@/bin/server"; import type { ServiceResponse } from "@/common/models/serviceResponse"; -import { app } from "@/server"; describe("Health Check API endpoints", () => { it("GET / - success", async () => { diff --git a/src/api/user/__tests__/userRouter.test.ts b/src/api/user/__tests__/userRouter.test.ts index ce2f92a..00f06bb 100644 --- a/src/api/user/__tests__/userRouter.test.ts +++ b/src/api/user/__tests__/userRouter.test.ts @@ -3,8 +3,8 @@ import request from "supertest"; import type { User } from "@/api/user/userModel"; import { users } from "@/api/user/userRepository"; +import { app } from "@/bin/server"; import type { ServiceResponse } from "@/common/models/serviceResponse"; -import { app } from "@/server"; describe("User API Endpoints", () => { describe("GET /users", () => { diff --git a/src/api/user/userService.ts b/src/api/user/userService.ts index dfb2491..22e88a0 100644 --- a/src/api/user/userService.ts +++ b/src/api/user/userService.ts @@ -2,8 +2,8 @@ import { StatusCodes } from "http-status-codes"; import type { User } from "@/api/user/userModel"; import { UserRepository } from "@/api/user/userRepository"; +import { logger } from "@/bin/server"; import { ServiceResponse } from "@/common/models/serviceResponse"; -import { logger } from "@/server"; export class UserService { private userRepository: UserRepository; diff --git a/src/api/welcome/__tests__/welcomeRouter.test.ts b/src/api/welcome/__tests__/welcomeRouter.test.ts new file mode 100644 index 0000000..bec11d8 --- /dev/null +++ b/src/api/welcome/__tests__/welcomeRouter.test.ts @@ -0,0 +1,14 @@ +import { StatusCodes } from "http-status-codes"; +import request from "supertest"; + +import { app } from "@/bin/server"; + +describe("Welcome API endpoints", () => { + it("GET / - success", async () => { + const response = await request(app).get("/"); + const result: string = response.text; + + expect(response.statusCode).toEqual(StatusCodes.OK); + expect(result).toEqual("

Welcome to Express Typescript Api

"); + }); +}); diff --git a/src/api/welcome/welcomeRouter.ts b/src/api/welcome/welcomeRouter.ts new file mode 100644 index 0000000..17d0980 --- /dev/null +++ b/src/api/welcome/welcomeRouter.ts @@ -0,0 +1,22 @@ +import { OpenAPIRegistry } from "@asteasolutions/zod-to-openapi"; +import express, { type Request, type Response, type Router } from "express"; +import { z } from "zod"; + +import { createApiResponse } from "@/api-docs/openAPIResponseBuilders"; +import { HtmlServiceResponse } from "@/common/models/htmlServiceResponse"; +import { handleHtmlServiceResponse } from "@/common/utils/httpHandlers"; + +export const welcomeRegistry = new OpenAPIRegistry(); +export const welcomeRouter: Router = express.Router(); + +welcomeRegistry.registerPath({ + method: "get", + path: "/", + tags: ["Welcome"], + responses: createApiResponse(z.null(), "Success"), +}); + +welcomeRouter.get("/", (_req: Request, res: Response) => { + const htmlServiceResponse = HtmlServiceResponse.success("

Welcome to Express Typescript Api

"); + return handleHtmlServiceResponse(htmlServiceResponse, res); +}); diff --git a/src/bin/logger.ts b/src/bin/logger.ts new file mode 100644 index 0000000..116386e --- /dev/null +++ b/src/bin/logger.ts @@ -0,0 +1,3 @@ +import { pino } from "pino"; + +export const logger = pino({ name: "Server Start" }); diff --git a/src/bin/routes.ts b/src/bin/routes.ts new file mode 100644 index 0000000..354d56e --- /dev/null +++ b/src/bin/routes.ts @@ -0,0 +1,16 @@ +import { openAPIRouter } from "@/api-docs/openAPIRouter"; +import { healthCheckRouter } from "@/api/healthCheck/healthCheckRouter"; +import { userRouter } from "@/api/user/userRouter"; +import { welcomeRouter } from "@/api/welcome/welcomeRouter"; +import express, { type Router } from "express"; + +export const router: Router = express.Router(); + +// Home +router.use("/", welcomeRouter); + +router.use("/health-check", healthCheckRouter); +router.use("/users", userRouter); + +// Swagger UI +router.use("/swagger", openAPIRouter); diff --git a/src/server.ts b/src/bin/server.ts similarity index 67% rename from src/server.ts rename to src/bin/server.ts index b286b5b..8413418 100644 --- a/src/server.ts +++ b/src/bin/server.ts @@ -1,17 +1,14 @@ import cors from "cors"; import express, { type Express } from "express"; import helmet from "helmet"; -import { pino } from "pino"; -import { openAPIRouter } from "@/api-docs/openAPIRouter"; -import { healthCheckRouter } from "@/api/healthCheck/healthCheckRouter"; -import { userRouter } from "@/api/user/userRouter"; +import { logger } from "@/bin/logger"; +import { router as globalRouter } from "@/bin/routes"; import errorHandler from "@/common/middleware/errorHandler"; import rateLimiter from "@/common/middleware/rateLimiter"; import requestLogger from "@/common/middleware/requestLogger"; import { env } from "@/common/utils/envConfig"; -const logger = pino({ name: "server start" }); const app: Express = express(); // Set the application to trust the reverse proxy @@ -28,11 +25,7 @@ app.use(rateLimiter); app.use(requestLogger); // Routes -app.use("/health-check", healthCheckRouter); -app.use("/users", userRouter); - -// Swagger UI -app.use(openAPIRouter); +app.use(globalRouter); // Error handlers app.use(errorHandler()); diff --git a/src/common/models/htmlServiceResponse.ts b/src/common/models/htmlServiceResponse.ts new file mode 100644 index 0000000..191d419 --- /dev/null +++ b/src/common/models/htmlServiceResponse.ts @@ -0,0 +1,26 @@ +import { StatusCodes } from "http-status-codes"; +import { z } from "zod"; + +export class HtmlServiceResponse { + readonly html: string; + readonly statusCode: number; + + private constructor(html: string, statusCode: number) { + this.html = html; + this.statusCode = statusCode; + } + + static success(html: string, statusCode: number = StatusCodes.OK) { + return new HtmlServiceResponse(html, statusCode); + } + + static failure(html: string, statusCode: number = StatusCodes.BAD_REQUEST) { + return new HtmlServiceResponse(html, statusCode); + } +} + +export const HtmlServiceResponseSchema = () => + z.object({ + html: z.string(), + statusCode: z.number(), + }); diff --git a/src/common/utils/httpHandlers.ts b/src/common/utils/httpHandlers.ts index 1464f6b..ea6d627 100644 --- a/src/common/utils/httpHandlers.ts +++ b/src/common/utils/httpHandlers.ts @@ -2,12 +2,17 @@ import type { NextFunction, Request, Response } from "express"; import { StatusCodes } from "http-status-codes"; import type { ZodError, ZodSchema } from "zod"; +import type { HtmlServiceResponse } from "@/common/models/htmlServiceResponse"; import { ServiceResponse } from "@/common/models/serviceResponse"; export const handleServiceResponse = (serviceResponse: ServiceResponse, response: Response) => { return response.status(serviceResponse.statusCode).send(serviceResponse); }; +export const handleHtmlServiceResponse = (serviceResponse: HtmlServiceResponse, response: Response) => { + return response.status(serviceResponse.statusCode).send(serviceResponse.html); +}; + export const validateRequest = (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => { try { schema.parse({ body: req.body, query: req.query, params: req.params }); diff --git a/src/index.ts b/src/index.ts index f9a7d0d..567cb3a 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ +import { app, logger } from "@/bin/server"; import { env } from "@/common/utils/envConfig"; -import { app, logger } from "@/server"; const server = app.listen(env.PORT, () => { const { NODE_ENV, HOST, PORT } = env;