@leo-h/create-nodejs-app 1.0.14 → 1.0.15

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 (43) hide show
  1. package/dist/package.json.js +1 -1
  2. package/package.json +1 -1
  3. package/templates/fastify/package.json +3 -3
  4. package/templates/fastify/src/core/domain-error.ts +17 -0
  5. package/templates/fastify/src/infra/http/controllers/hello/{hello-multipart.controller.e2e-spec.ts → hello-multipart.controller.integration-spec.ts} +5 -4
  6. package/templates/fastify/src/infra/http/controllers/hello/hello-multipart.controller.ts +1 -1
  7. package/templates/fastify/src/infra/http/controllers/hello/hello.controller.integration-spec.ts +38 -0
  8. package/templates/fastify/src/infra/http/controllers/hello/hello.controller.ts +2 -0
  9. package/templates/fastify/src/infra/http/errors/bad-request.error.ts +4 -4
  10. package/templates/fastify/src/infra/http/errors/internal-server.error.ts +13 -0
  11. package/templates/fastify/src/infra/http/errors/upload-validation.error.ts +12 -6
  12. package/templates/fastify/src/infra/http/errors/validation.error.ts +13 -0
  13. package/templates/fastify/src/infra/http/plugins/error-handler.plugin.ts +21 -35
  14. package/templates/fastify/src/infra/http/routes.ts +8 -0
  15. package/templates/fastify/src/infra/presenters/error.presenter.ts +4 -5
  16. package/templates/fastify/{vitest.config.e2e.mts → vitest.config.integration.mts} +2 -2
  17. package/templates/nest/package.json +3 -3
  18. package/templates/{fastify/src/core/errors → nest/src/core}/domain-error.ts +5 -0
  19. package/templates/nest/src/infra/http/controllers/hello/{hello-multipart.controller.e2e-spec.ts → hello-multipart.controller.integration-spec.ts} +5 -4
  20. package/templates/nest/src/infra/http/controllers/hello/hello-multipart.controller.ts +4 -4
  21. package/templates/nest/src/infra/http/controllers/hello/hello.controller.integration-spec.ts +62 -0
  22. package/templates/nest/src/infra/http/controllers/hello/hello.controller.ts +5 -12
  23. package/templates/nest/src/infra/http/errors/bad-request.error.ts +18 -0
  24. package/templates/nest/src/infra/http/errors/filters/all-exception.filter.ts +11 -11
  25. package/templates/nest/src/infra/http/errors/internal-server.error.ts +11 -8
  26. package/templates/nest/src/infra/http/errors/upload-validation.error.ts +16 -10
  27. package/templates/nest/src/infra/http/errors/validation.error.ts +21 -0
  28. package/templates/nest/src/infra/http/http.module.ts +0 -5
  29. package/templates/nest/src/infra/http/middlewares/zod-validation-pipe.ts +1 -1
  30. package/templates/nest/src/infra/presenters/error.presenter.ts +2 -4
  31. package/templates/nest/{vitest.config.e2e.mts → vitest.config.integration.mts} +1 -1
  32. package/templates/fastify/src/core/errors/errors.ts +0 -9
  33. package/templates/fastify/src/infra/http/controllers/hello/hello.controller.e2e-spec.ts +0 -14
  34. package/templates/fastify/src/infra/http/errors/http-error.ts +0 -9
  35. package/templates/fastify/src/infra/http/routes/hello.routes.ts +0 -8
  36. package/templates/fastify/src/infra/http/routes/index.ts +0 -6
  37. package/templates/nest/src/core/errors/domain-error.ts +0 -8
  38. package/templates/nest/src/core/errors/validation.error.ts +0 -12
  39. package/templates/nest/src/infra/http/controllers/hello/hello.controller.e2e-spec.ts +0 -38
  40. package/templates/nest/src/infra/http/errors/filters/domain-exception.filter.ts +0 -39
  41. /package/templates/fastify/test/{e2e → integration}/sample-upload.jpg +0 -0
  42. /package/templates/fastify/test/{e2e → integration}/setup.ts +0 -0
  43. /package/templates/nest/test/{e2e → integration}/sample-upload.jpg +0 -0
@@ -1 +1 @@
1
- Object.defineProperty(exports,"__esModule",{value:!0});const name="@leo-h/create-nodejs-app",version="1.0.14",packageManager="pnpm@9.1.1",author="Leonardo Henrique <leonardo0507.business@gmail.com>",description="Create a modern Node.js app with TypeScript using one command.",license="MIT",keywords=["node","node.js","typescript"],bin={"create-nodejs-app":"./dist/index.js"},files=["./dist","./templates"],repository={type:"git",url:"https://github.com/Leo-Henrique/create-nodejs-app"},scripts={prepare:"husky",start:"node ./dist/index.js","start:dev":"tsx ./src/index.ts","start:dev:watch":"tsx watch ./src/index.ts",typecheck:"tsc --noEmit",lint:"eslint . --ext .ts --max-warnings 0 --cache","lint:fix":"pnpm lint --fix",format:"prettier . --write --cache","test:unit":"vitest run","test:unit:watch":"vitest","test:e2e":"vitest run --config ./vitest.config.e2e.mts","test:e2e:watch":"vitest --config ./vitest.config.e2e.mts","test:coverage":"vitest run --coverage.enabled=true",template:"tsx ./scripts/template-cli.ts",prebuild:"rimraf ./dist",build:"unbuild",prepublishOnly:"pnpm build"},dependencies={commander:"12.1.0",picocolors:"1.0.1",prompts:"2.4.2","validate-npm-package-name":"5.0.1",zod:"3.23.8"},devDependencies={"@faker-js/faker":"8.4.1","@types/node":"20.12.12","@types/prompts":"2.4.9","@types/validate-npm-package-name":"4.0.2","@typescript-eslint/eslint-plugin":"7.10.0","@typescript-eslint/parser":"7.10.0","conventional-changelog-conventionalcommits":"8.0.0",eslint:"8.57.0","eslint-config-prettier":"9.1.0","eslint-plugin-vitest":"0.4.0",husky:"9.0.11","lint-staged":"15.2.2","npm-run-all":"4.1.5",prettier:"3.2.5",rimraf:"5.0.7",tsx:"4.10.5",typescript:"5.4.5",unbuild:"2.0.0","vite-tsconfig-paths":"4.3.2",vitest:"1.6.0"},d={name,version,packageManager,author,description,license,keywords,bin,files,repository,scripts,dependencies,devDependencies};exports.author=author;exports.bin=bin;exports.default=d;exports.dependencies=dependencies;exports.description=description;exports.devDependencies=devDependencies;exports.files=files;exports.keywords=keywords;exports.license=license;exports.name=name;exports.packageManager=packageManager;exports.repository=repository;exports.scripts=scripts;exports.version=version;
1
+ Object.defineProperty(exports,"__esModule",{value:!0});const name="@leo-h/create-nodejs-app",version="1.0.15",packageManager="pnpm@9.1.1",author="Leonardo Henrique <leonardo0507.business@gmail.com>",description="Create a modern Node.js app with TypeScript using one command.",license="MIT",keywords=["node","node.js","typescript"],bin={"create-nodejs-app":"./dist/index.js"},files=["./dist","./templates"],repository={type:"git",url:"https://github.com/Leo-Henrique/create-nodejs-app"},scripts={prepare:"husky",start:"node ./dist/index.js","start:dev":"tsx ./src/index.ts","start:dev:watch":"tsx watch ./src/index.ts",typecheck:"tsc --noEmit",lint:"eslint . --ext .ts --max-warnings 0 --cache","lint:fix":"pnpm lint --fix",format:"prettier . --write --cache","test:unit":"vitest run","test:unit:watch":"vitest","test:e2e":"vitest run --config ./vitest.config.e2e.mts","test:e2e:watch":"vitest --config ./vitest.config.e2e.mts","test:coverage":"vitest run --coverage.enabled=true",template:"tsx ./scripts/template-cli.ts",prebuild:"rimraf ./dist",build:"unbuild",prepublishOnly:"pnpm build"},dependencies={commander:"12.1.0",picocolors:"1.0.1",prompts:"2.4.2","validate-npm-package-name":"5.0.1",zod:"3.23.8"},devDependencies={"@faker-js/faker":"8.4.1","@types/node":"20.12.12","@types/prompts":"2.4.9","@types/validate-npm-package-name":"4.0.2","@typescript-eslint/eslint-plugin":"7.10.0","@typescript-eslint/parser":"7.10.0","conventional-changelog-conventionalcommits":"8.0.0",eslint:"8.57.0","eslint-config-prettier":"9.1.0","eslint-plugin-vitest":"0.4.0",husky:"9.0.11","lint-staged":"15.2.2","npm-run-all":"4.1.5",prettier:"3.2.5",rimraf:"5.0.7",tsx:"4.10.5",typescript:"5.4.5",unbuild:"2.0.0","vite-tsconfig-paths":"4.3.2",vitest:"1.6.0"},d={name,version,packageManager,author,description,license,keywords,bin,files,repository,scripts,dependencies,devDependencies};exports.author=author;exports.bin=bin;exports.default=d;exports.dependencies=dependencies;exports.description=description;exports.devDependencies=devDependencies;exports.files=files;exports.keywords=keywords;exports.license=license;exports.name=name;exports.packageManager=packageManager;exports.repository=repository;exports.scripts=scripts;exports.version=version;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo-h/create-nodejs-app",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "packageManager": "pnpm@9.1.1",
5
5
  "author": "Leonardo Henrique <leonardo0507.business@gmail.com>",
6
6
  "description": "Create a modern Node.js app with TypeScript using one command.",
@@ -13,9 +13,9 @@
13
13
  "test:unit": "vitest run",
14
14
  "test:unit:watch": "vitest",
15
15
  "test:unit:coverage": "vitest run --coverage.enabled=true",
16
- "test:e2e": "vitest run --config ./vitest.config.e2e.mts",
17
- "test:e2e:watch": "vitest --config ./vitest.config.e2e.mts",
18
- "test:e2e:coverage": "vitest run --config ./vitest.config.e2e.mts --coverage.enabled=true",
16
+ "test:integration": "vitest run --config ./vitest.config.integration.mts",
17
+ "test:integration:watch": "vitest --config ./vitest.config.integration.mts",
18
+ "test:integration:coverage": "vitest run --config ./vitest.config.integration.mts --coverage.enabled=true",
19
19
  "prebuild": "rimraf ./dist",
20
20
  "build": "unbuild"
21
21
  },
@@ -0,0 +1,17 @@
1
+ export type DomainErrorCoreProperties = Pick<
2
+ DomainError,
3
+ "error" | "message" | "debug"
4
+ >;
5
+
6
+ export abstract class DomainError extends Error {
7
+ public abstract error: string;
8
+ public abstract debug: unknown;
9
+
10
+ constructor(public message: string) {
11
+ super(message);
12
+ }
13
+ }
14
+
15
+ export abstract class HttpError extends DomainError {
16
+ public abstract statusCode: number;
17
+ }
@@ -4,18 +4,19 @@ import { lookup } from "mime-types";
4
4
  import { basename, extname } from "path";
5
5
  import request from "supertest";
6
6
  import { describe, expect, it } from "vitest";
7
+ import { HelloMultipartControllerBody } from "./hello-multipart.controller";
7
8
 
8
- const SAMPLE_UPLOAD_PATH = "./test/e2e/sample-upload.jpg";
9
+ const SAMPLE_UPLOAD_PATH = "./test/integration/sample-upload.jpg";
9
10
 
10
- describe("[Controller] Hello multipart", () => {
11
- it("[POST] /hello/multipart", async () => {
11
+ describe("[Controller] POST /hello/multipart", () => {
12
+ it("should be able to upload a image", async () => {
12
13
  const fieldName = "attachment";
13
14
  const description = faker.lorem.sentence();
14
15
 
15
16
  const response = await request(app.server)
16
17
  .post("/hello/multipart")
17
18
  .attach(fieldName, SAMPLE_UPLOAD_PATH)
18
- .field({ description });
19
+ .field({ description } satisfies HelloMultipartControllerBody);
19
20
 
20
21
  expect(response.statusCode).toEqual(200);
21
22
  expect(response.body).toEqual(
@@ -7,7 +7,7 @@ const helloMultipartControllerBodySchema = z.object({
7
7
  description: z.string().min(2),
8
8
  });
9
9
 
10
- type HelloMultipartControllerBody = z.infer<
10
+ export type HelloMultipartControllerBody = z.infer<
11
11
  typeof helloMultipartControllerBodySchema
12
12
  >;
13
13
 
@@ -0,0 +1,38 @@
1
+ import { app } from "@/infra/http/app";
2
+ import request from "supertest";
3
+ import { describe, expect, it } from "vitest";
4
+ import { HelloControllerQuery } from "./hello.controller";
5
+
6
+ describe("[Controller] GET /hello", () => {
7
+ it("should be able to show hello world when setting true in the query parameter", async () => {
8
+ const response = await request(app.server)
9
+ .get("/hello")
10
+ .query({ show: true } satisfies HelloControllerQuery);
11
+
12
+ expect(response.statusCode).toEqual(200);
13
+ expect(response.body).toStrictEqual({ message: "Hello world!" });
14
+ });
15
+
16
+ it("should not be able to show hello world when setting false in the query parameter", async () => {
17
+ const response = await request(app.server)
18
+ .get("/hello")
19
+ .query({ show: false } satisfies HelloControllerQuery);
20
+
21
+ expect(response.statusCode).toEqual(400);
22
+ expect(response.body.error).toEqual("BAD_REQUEST_ERROR");
23
+ });
24
+
25
+ describe("Input data validations", () => {
26
+ it("with invalid show property", async () => {
27
+ const response = await request(app.server)
28
+ .get("/hello")
29
+ .query({
30
+ // @ts-expect-error the value must be a boolean
31
+ show: 0,
32
+ } satisfies HelloControllerQuery);
33
+
34
+ expect(response.statusCode).toEqual(400);
35
+ expect(response.body.error).toEqual("VALIDATION_ERROR");
36
+ });
37
+ });
38
+ });
@@ -10,6 +10,8 @@ const helloControllerQuerySchema = z.object({
10
10
  .default("true"),
11
11
  });
12
12
 
13
+ export type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
14
+
13
15
  export async function helloController(app: FastifyInstance) {
14
16
  app.withTypeProvider<ZodTypeProvider>().route({
15
17
  method: "GET",
@@ -1,9 +1,9 @@
1
- import { HttpError } from "./http-error";
1
+ import { HttpError } from "@/core/domain-error";
2
2
 
3
3
  export class BadRequestError extends HttpError {
4
- public error = "BadRequestError";
5
- public statusCode = 400;
6
- public debug = null;
4
+ readonly error = "BAD_REQUEST_ERROR";
5
+ readonly statusCode = 400;
6
+ readonly debug = null;
7
7
 
8
8
  constructor(public message: string) {
9
9
  super(message);
@@ -0,0 +1,13 @@
1
+ import { HttpError } from "@/core/domain-error";
2
+
3
+ const message = "Desculpe, um erro inesperado ocorreu.";
4
+
5
+ export class InternalServerError extends HttpError {
6
+ readonly statusCode = 500;
7
+ readonly error = "INTERNAL_SERVER_ERROR";
8
+ readonly message = message;
9
+
10
+ constructor(public debug: unknown) {
11
+ super(message);
12
+ }
13
+ }
@@ -1,15 +1,21 @@
1
- import { HttpError } from "./http-error";
1
+ import { HttpError } from "@/core/domain-error";
2
+
3
+ const message = "Os dados enviados são inválidos.";
2
4
 
3
5
  export class UploadValidationError extends HttpError {
4
- public error = "UploadValidationError";
5
- public debug = { multerError: null };
6
+ readonly error = "UPLOAD_VALIDATION_ERROR";
7
+ readonly message = message;
8
+ public debug: object;
6
9
 
7
10
  constructor(
8
11
  public statusCode = 400,
9
- debug: object = {},
12
+ debug = {},
10
13
  ) {
11
- super("Os dados enviados são inválidos.");
14
+ super(message);
12
15
 
13
- this.debug = { ...this.debug, ...debug };
16
+ this.debug = {
17
+ multerError: null,
18
+ ...debug,
19
+ };
14
20
  }
15
21
  }
@@ -0,0 +1,13 @@
1
+ import { HttpError } from "@/core/domain-error";
2
+
3
+ export class ValidationError extends HttpError {
4
+ readonly statusCode = 400;
5
+ readonly error = "VALIDATION_ERROR";
6
+
7
+ constructor(
8
+ public debug: unknown = null,
9
+ public message = "Os dados enviados são inválidos.",
10
+ ) {
11
+ super(message);
12
+ }
13
+ }
@@ -1,45 +1,31 @@
1
- import { ValidationError } from "@/core/errors/errors";
2
- import { env } from "@/infra/env";
1
+ import { HttpError } from "@/core/domain-error";
3
2
  import { ErrorPresenter } from "@/infra/presenters/error.presenter";
4
- import { FastifyError, FastifyReply, FastifyRequest } from "fastify";
3
+ import { FastifyReply, FastifyRequest } from "fastify";
5
4
  import { ZodError } from "zod";
6
- import { HttpError } from "../errors/http-error";
5
+ import { InternalServerError } from "../errors/internal-server.error";
6
+ import { ValidationError } from "../errors/validation.error";
7
7
 
8
8
  export async function errorHandlerPlugin(
9
- error: FastifyError,
9
+ error: unknown,
10
10
  _req: FastifyRequest,
11
11
  res: FastifyReply,
12
12
  ) {
13
- console.error(error);
14
-
15
- let httpResponse: ReturnType<typeof ErrorPresenter.toHttp> =
16
- ErrorPresenter.toHttp(500, {
17
- error: "InternalServerError",
18
- message: "Desculpe, um erro inesperado ocorreu.",
19
- debug: error.message,
20
- });
21
-
22
- if (error.statusCode)
23
- httpResponse = ErrorPresenter.toHttp(error.statusCode, {
24
- error: error.name,
25
- message: error.message,
26
- debug: null,
27
- });
28
-
29
- if (error instanceof ValidationError)
30
- httpResponse = ErrorPresenter.toHttp(400, error);
31
-
32
- if (error instanceof ZodError)
33
- httpResponse = ErrorPresenter.toHttp(
34
- 400,
35
- new ValidationError(error.flatten().fieldErrors),
36
- );
37
-
38
- if (error instanceof HttpError)
39
- httpResponse = ErrorPresenter.toHttp(error.statusCode, error);
40
-
41
- if (env.NODE_ENV === "production" && "debug" in httpResponse)
42
- delete httpResponse.debug;
13
+ let httpError: HttpError;
14
+
15
+ if (error instanceof HttpError) httpError = error;
16
+ else if (error instanceof ZodError)
17
+ httpError = new ValidationError(error.flatten().fieldErrors);
18
+ else {
19
+ const debugFromUnknownError =
20
+ error && typeof error === "object" && "message" in error
21
+ ? error.message
22
+ : null;
23
+
24
+ httpError = new InternalServerError(debugFromUnknownError);
25
+ console.error(httpError);
26
+ }
27
+
28
+ const httpResponse = ErrorPresenter.toHttp(httpError.statusCode, httpError);
43
29
 
44
30
  res.status(httpResponse.statusCode).send(httpResponse);
45
31
  }
@@ -0,0 +1,8 @@
1
+ import { FastifyInstance } from "fastify";
2
+ import { helloController } from "./controllers/hello/hello.controller";
3
+ import { helloMultipartController } from "./controllers/hello/hello-multipart.controller";
4
+
5
+ export async function routes(app: FastifyInstance) {
6
+ app.register(helloController);
7
+ app.register(helloMultipartController);
8
+ }
@@ -1,14 +1,13 @@
1
- import { DomainError } from "@/core/errors/domain-error";
2
-
3
- type CustomError = Pick<DomainError, "error" | "message" | "debug">;
1
+ import { DomainErrorCoreProperties } from "@/core/domain-error";
2
+ import { env } from "../env";
4
3
 
5
4
  export class ErrorPresenter {
6
- public static toHttp(statusCode: number, error: DomainError | CustomError) {
5
+ public static toHttp(statusCode: number, error: DomainErrorCoreProperties) {
7
6
  return {
8
7
  error: error.error,
9
8
  message: error.message,
10
9
  statusCode,
11
- debug: error.debug,
10
+ debug: env.NODE_ENV !== "production" ? error.debug : null,
12
11
  };
13
12
  }
14
13
  }
@@ -3,7 +3,7 @@ import defaultConfig from "./vitest.config.mjs";
3
3
 
4
4
  export default mergeConfig(defaultConfig, {
5
5
  test: {
6
- include: ["./**/*.e2e-spec.ts"],
7
- setupFiles: ["./test/e2e/setup.ts"],
6
+ include: ["./**/*.integration-spec.ts"],
7
+ setupFiles: ["./test/integration/setup.ts"],
8
8
  },
9
9
  });
@@ -13,9 +13,9 @@
13
13
  "test:unit": "vitest run",
14
14
  "test:unit:watch": "vitest",
15
15
  "test:unit:coverage": "vitest run --coverage.enabled=true",
16
- "test:e2e": "vitest run --config ./vitest.config.e2e.mts",
17
- "test:e2e:watch": "vitest --config ./vitest.config.e2e.mts",
18
- "test:e2e:coverage": "vitest run --config ./vitest.config.e2e.mts --coverage.enabled=true",
16
+ "test:integration": "vitest run --config ./vitest.config.integration.mts",
17
+ "test:integration:watch": "vitest --config ./vitest.config.integration.mts",
18
+ "test:integration:coverage": "vitest run --config ./vitest.config.integration.mts --coverage.enabled=true",
19
19
  "build": "nest build"
20
20
  },
21
21
  "dependencies": {
@@ -1,3 +1,8 @@
1
+ export type DomainErrorCoreProperties = Pick<
2
+ DomainError,
3
+ "error" | "message" | "debug"
4
+ >;
5
+
1
6
  export abstract class DomainError extends Error {
2
7
  public abstract error: string;
3
8
  public abstract debug: unknown;
@@ -9,10 +9,11 @@ import { lookup } from "mime-types";
9
9
  import { basename, extname } from "path";
10
10
  import request from "supertest";
11
11
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
12
+ import { HelloMultipartControllerBody } from "./hello-multipart.controller";
12
13
 
13
- const SAMPLE_UPLOAD_PATH = "./test/e2e/sample-upload.jpg";
14
+ const SAMPLE_UPLOAD_PATH = "./test/integration/sample-upload.jpg";
14
15
 
15
- describe("[Controller] Hello multipart", () => {
16
+ describe("[Controller] POST /hello/multipart", () => {
16
17
  let app: NestFastifyApplication;
17
18
 
18
19
  beforeAll(async () => {
@@ -32,14 +33,14 @@ describe("[Controller] Hello multipart", () => {
32
33
  await app.close();
33
34
  });
34
35
 
35
- it("[POST] /hello/multipart", async () => {
36
+ it("should be able to upload a image", async () => {
36
37
  const fieldName = "attachment";
37
38
  const description = faker.lorem.sentence();
38
39
 
39
40
  const response = await request(app.getHttpServer())
40
41
  .post("/hello/multipart")
41
42
  .attach(fieldName, SAMPLE_UPLOAD_PATH)
42
- .field({ description });
43
+ .field({ description } satisfies HelloMultipartControllerBody);
43
44
 
44
45
  expect(response.statusCode).toEqual(200);
45
46
  expect(response.body).toEqual(
@@ -9,10 +9,6 @@ const helloMultipartControllerBodySchema = z.object({
9
9
  description: z.string().min(2),
10
10
  });
11
11
 
12
- type HelloMultipartControllerBody = z.infer<
13
- typeof helloMultipartControllerBodySchema
14
- >;
15
-
16
12
  const helloMultipartControllerResponseSchema = z.object({
17
13
  message: z.literal("Hello world!"),
18
14
  description: z.string().min(2),
@@ -25,6 +21,10 @@ const helloMultipartControllerResponseSchema = z.object({
25
21
  }),
26
22
  });
27
23
 
24
+ export type HelloMultipartControllerBody = z.infer<
25
+ typeof helloMultipartControllerBodySchema
26
+ >;
27
+
28
28
  type HelloMultipartControllerResponse = z.infer<
29
29
  typeof helloMultipartControllerResponseSchema
30
30
  >;
@@ -0,0 +1,62 @@
1
+ import { AppModule } from "@/infra/app.module";
2
+ import {
3
+ FastifyAdapter,
4
+ NestFastifyApplication,
5
+ } from "@nestjs/platform-fastify";
6
+ import { Test } from "@nestjs/testing";
7
+ import request from "supertest";
8
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
9
+ import { HelloControllerQuery } from "./hello.controller";
10
+
11
+ describe("[Controller] GET /hello", () => {
12
+ let app: NestFastifyApplication;
13
+
14
+ beforeAll(async () => {
15
+ const moduleRef = await Test.createTestingModule({
16
+ imports: [AppModule],
17
+ }).compile();
18
+
19
+ app = moduleRef.createNestApplication<NestFastifyApplication>(
20
+ new FastifyAdapter(),
21
+ );
22
+
23
+ await app.init();
24
+ await app.getHttpAdapter().getInstance().ready();
25
+ });
26
+
27
+ afterAll(async () => {
28
+ await app.close();
29
+ });
30
+
31
+ it("should be able to show hello world when setting true in the query parameter", async () => {
32
+ const response = await request(app.getHttpServer())
33
+ .get("/hello")
34
+ .query({ show: true } satisfies HelloControllerQuery);
35
+
36
+ expect(response.statusCode).toEqual(200);
37
+ expect(response.body).toStrictEqual({ message: "Hello world!" });
38
+ });
39
+
40
+ it("should not be able to show hello world when setting false in the query parameter", async () => {
41
+ const response = await request(app.getHttpServer())
42
+ .get("/hello")
43
+ .query({ show: false } satisfies HelloControllerQuery);
44
+
45
+ expect(response.statusCode).toEqual(400);
46
+ expect(response.body.error).toEqual("BAD_REQUEST_ERROR");
47
+ });
48
+
49
+ describe("Input data validations", () => {
50
+ it("with invalid show property", async () => {
51
+ const response = await request(app.getHttpServer())
52
+ .get("/hello")
53
+ .query({
54
+ // @ts-expect-error the value must be a boolean
55
+ show: 0,
56
+ } satisfies HelloControllerQuery);
57
+
58
+ expect(response.statusCode).toEqual(400);
59
+ expect(response.body.error).toEqual("VALIDATION_ERROR");
60
+ });
61
+ });
62
+ });
@@ -1,12 +1,7 @@
1
- import {
2
- BadRequestException,
3
- Controller,
4
- Get,
5
- HttpCode,
6
- Query,
7
- } from "@nestjs/common";
1
+ import { Controller, Get, HttpCode, Query } from "@nestjs/common";
8
2
  import { ApiOperation, ApiTags } from "@nestjs/swagger";
9
3
  import { z } from "zod";
4
+ import { BadRequestError } from "../../errors/bad-request.error";
10
5
  import { ZodSchemaPipe } from "../../middlewares/zod-schema-pipe";
11
6
 
12
7
  export const helloControllerQuerySchema = z.object({
@@ -16,12 +11,12 @@ export const helloControllerQuerySchema = z.object({
16
11
  .transform<boolean>(val => JSON.parse(val)),
17
12
  });
18
13
 
19
- type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
20
-
21
14
  const helloControllerResponseSchema = z.object({
22
15
  message: z.literal("Hello world!"),
23
16
  });
24
17
 
18
+ export type HelloControllerQuery = z.infer<typeof helloControllerQuerySchema>;
19
+
25
20
  type HelloControllerResponse = z.infer<typeof helloControllerResponseSchema>;
26
21
 
27
22
  @Controller()
@@ -42,9 +37,7 @@ export class HelloController {
42
37
  const { show } = query;
43
38
 
44
39
  if (!show)
45
- throw new BadRequestException(
46
- `You don't want to display the "hello world"!`,
47
- );
40
+ throw new BadRequestError(`You don't want to display the "hello world"!`);
48
41
 
49
42
  return { message: "Hello world!" };
50
43
  }
@@ -0,0 +1,18 @@
1
+ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
2
+ import { HttpException } from "@nestjs/common";
3
+
4
+ export class BadRequestError extends HttpException {
5
+ static readonly statusCode = 400;
6
+ static readonly error = "BAD_REQUEST_ERROR";
7
+ static readonly debug = null;
8
+
9
+ constructor(public message: string) {
10
+ super(
11
+ ErrorPresenter.toHttp(BadRequestError.statusCode, {
12
+ ...BadRequestError,
13
+ message,
14
+ }),
15
+ BadRequestError.statusCode,
16
+ );
17
+ }
18
+ }
@@ -13,19 +13,19 @@ export class AllExceptionFilter implements ExceptionFilter {
13
13
  const ctx = host.switchToHttp();
14
14
  const response = ctx.getResponse<FastifyReply>();
15
15
 
16
- const debug =
17
- exception && typeof exception === "object" && "message" in exception
18
- ? exception.message
19
- : null;
16
+ let httpError: HttpException;
20
17
 
21
- let httpException: HttpException = new InternalServerError(debug);
18
+ if (exception instanceof HttpException) httpError = exception;
19
+ else {
20
+ const debugFromUnknownError =
21
+ exception && typeof exception === "object" && "message" in exception
22
+ ? exception.message
23
+ : null;
22
24
 
23
- if (exception instanceof HttpException) httpException = exception;
25
+ httpError = new InternalServerError(debugFromUnknownError);
26
+ console.error(httpError);
27
+ }
24
28
 
25
- console.error(exception);
26
-
27
- response
28
- .status(httpException.getStatus())
29
- .send(httpException.getResponse());
29
+ response.status(httpError.getStatus()).send(httpError.getResponse());
30
30
  }
31
31
  }
@@ -2,14 +2,17 @@ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
2
2
  import { HttpException } from "@nestjs/common";
3
3
 
4
4
  export class InternalServerError extends HttpException {
5
- constructor(debug: unknown) {
6
- const statusCode = 500;
7
- const presenter = ErrorPresenter.toHttp(statusCode, {
8
- error: "InternalServerError",
9
- message: "Desculpe, um erro inesperado ocorreu.",
10
- debug,
11
- });
5
+ static readonly statusCode = 500;
6
+ static readonly error = "INTERNAL_SERVER_ERROR";
7
+ static readonly message = "Desculpe, um erro inesperado ocorreu.";
12
8
 
13
- super(presenter, statusCode);
9
+ constructor(debug: unknown) {
10
+ super(
11
+ ErrorPresenter.toHttp(InternalServerError.statusCode, {
12
+ ...InternalServerError,
13
+ debug,
14
+ }),
15
+ InternalServerError.statusCode,
16
+ );
14
17
  }
15
18
  }
@@ -2,16 +2,22 @@ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
2
2
  import { HttpException } from "@nestjs/common";
3
3
 
4
4
  export class UploadValidationError extends HttpException {
5
- constructor(statusCode = 400, debug: object = {}) {
6
- const presenter = ErrorPresenter.toHttp(statusCode, {
7
- error: "UploadValidationError",
8
- message: "Os dados enviados são inválidos.",
9
- debug: {
10
- multerError: null,
11
- ...debug,
12
- },
13
- });
5
+ static readonly error = "UPLOAD_VALIDATION_ERROR";
14
6
 
15
- super(presenter, statusCode);
7
+ constructor(
8
+ public statusCode = 400,
9
+ public debug = {},
10
+ ) {
11
+ super(
12
+ ErrorPresenter.toHttp(statusCode, {
13
+ ...UploadValidationError,
14
+ message: "Os dados enviados são inválidos.",
15
+ debug: {
16
+ multerError: null,
17
+ ...debug,
18
+ },
19
+ }),
20
+ statusCode,
21
+ );
16
22
  }
17
23
  }
@@ -0,0 +1,21 @@
1
+ import { ErrorPresenter } from "@/infra/presenters/error.presenter";
2
+ import { HttpException } from "@nestjs/common";
3
+
4
+ export class ValidationError extends HttpException {
5
+ static readonly statusCode = 400;
6
+ static readonly error = "VALIDATION_ERROR";
7
+
8
+ constructor(
9
+ public debug: unknown = null,
10
+ public message = "Os dados enviados são inválidos.",
11
+ ) {
12
+ super(
13
+ ErrorPresenter.toHttp(ValidationError.statusCode, {
14
+ ...ValidationError,
15
+ message,
16
+ debug,
17
+ }),
18
+ ValidationError.statusCode,
19
+ );
20
+ }
21
+ }
@@ -3,7 +3,6 @@ import { APP_FILTER } from "@nestjs/core";
3
3
  import { HelloMultipartController } from "./controllers/hello/hello-multipart.controller";
4
4
  import { HelloController } from "./controllers/hello/hello.controller";
5
5
  import { AllExceptionFilter } from "./errors/filters/all-exception.filter";
6
- import { DomainExceptionFilter } from "./errors/filters/domain-exception.filter";
7
6
  import { FastifyMulterEventModule } from "./events/fastify-multer.event.module";
8
7
 
9
8
  @Module({
@@ -13,10 +12,6 @@ import { FastifyMulterEventModule } from "./events/fastify-multer.event.module";
13
12
  provide: APP_FILTER,
14
13
  useClass: AllExceptionFilter,
15
14
  },
16
- {
17
- provide: APP_FILTER,
18
- useClass: DomainExceptionFilter,
19
- },
20
15
  ],
21
16
  controllers: [HelloController, HelloMultipartController],
22
17
  })
@@ -1,6 +1,6 @@
1
- import { ValidationError } from "@/core/errors/validation.error";
2
1
  import { ArgumentMetadata, PipeTransform } from "@nestjs/common";
3
2
  import { ZodError, ZodObject, ZodRawShape, ZodType } from "zod";
3
+ import { ValidationError } from "../errors/validation.error";
4
4
 
5
5
  export interface ZodValidationPipeSchemas {
6
6
  routeParams?: ZodObject<ZodRawShape>;
@@ -1,10 +1,8 @@
1
- import { DomainError } from "@/core/errors/domain-error";
1
+ import { DomainErrorCoreProperties } from "@/core/domain-error";
2
2
  import { env } from "../env";
3
3
 
4
- type CustomError = Pick<DomainError, "error" | "message" | "debug">;
5
-
6
4
  export class ErrorPresenter {
7
- public static toHttp(statusCode: number, error: DomainError | CustomError) {
5
+ public static toHttp(statusCode: number, error: DomainErrorCoreProperties) {
8
6
  return {
9
7
  error: error.error,
10
8
  message: error.message,
@@ -3,6 +3,6 @@ import defaultConfig from "./vitest.config.mjs";
3
3
 
4
4
  export default mergeConfig(defaultConfig, {
5
5
  test: {
6
- include: ["./**/*.e2e-spec.ts"],
6
+ include: ["./**/*.integration-spec.ts"],
7
7
  },
8
8
  });
@@ -1,9 +0,0 @@
1
- import { DomainError } from "./domain-error";
2
-
3
- export class ValidationError extends DomainError {
4
- public error = "ValidationError";
5
-
6
- constructor(public debug: object | null = null) {
7
- super("Os dados enviados são inválidos.");
8
- }
9
- }
@@ -1,14 +0,0 @@
1
- import { app } from "@/infra/http/app";
2
- import request from "supertest";
3
- import { describe, expect, it } from "vitest";
4
-
5
- describe("[Controller] Hello", () => {
6
- it("[GET] /hello", async () => {
7
- const response = await request(app.server)
8
- .get("/hello")
9
- .query({ show: true });
10
-
11
- expect(response.statusCode).toEqual(200);
12
- expect(response.body).toEqual({ message: "Hello world!" });
13
- });
14
- });
@@ -1,9 +0,0 @@
1
- export abstract class HttpError extends Error {
2
- public abstract error: string;
3
- public abstract statusCode: number;
4
- public abstract debug: unknown;
5
-
6
- constructor(public message: string) {
7
- super(message);
8
- }
9
- }
@@ -1,8 +0,0 @@
1
- import { helloMultipartController } from "@/infra/http/controllers/hello/hello-multipart.controller";
2
- import { helloController } from "@/infra/http/controllers/hello/hello.controller";
3
- import { FastifyInstance } from "fastify";
4
-
5
- export async function helloRoutes(app: FastifyInstance) {
6
- app.register(helloController);
7
- app.register(helloMultipartController);
8
- }
@@ -1,6 +0,0 @@
1
- import { FastifyInstance } from "fastify";
2
- import { helloRoutes } from "./hello.routes";
3
-
4
- export async function routes(app: FastifyInstance) {
5
- app.register(helloRoutes);
6
- }
@@ -1,8 +0,0 @@
1
- export abstract class DomainError extends Error {
2
- public abstract error: string;
3
- public abstract debug: unknown;
4
-
5
- constructor(public message: string) {
6
- super(message);
7
- }
8
- }
@@ -1,12 +0,0 @@
1
- import { DomainError } from "./domain-error";
2
-
3
- export class ValidationError extends DomainError {
4
- public error = "ValidationError" as const;
5
- public debug: object | null;
6
-
7
- constructor(debug?: object) {
8
- super("Os dados recebidos são inválidos.");
9
-
10
- this.debug = debug ?? null;
11
- }
12
- }
@@ -1,38 +0,0 @@
1
- import { AppModule } from "@/infra/app.module";
2
- import {
3
- FastifyAdapter,
4
- NestFastifyApplication,
5
- } from "@nestjs/platform-fastify";
6
- import { Test } from "@nestjs/testing";
7
- import request from "supertest";
8
- import { afterAll, beforeAll, describe, expect, it } from "vitest";
9
-
10
- describe("[Controller] Hello", () => {
11
- let app: NestFastifyApplication;
12
-
13
- beforeAll(async () => {
14
- const moduleRef = await Test.createTestingModule({
15
- imports: [AppModule],
16
- }).compile();
17
-
18
- app = moduleRef.createNestApplication<NestFastifyApplication>(
19
- new FastifyAdapter(),
20
- );
21
-
22
- await app.init();
23
- await app.getHttpAdapter().getInstance().ready();
24
- });
25
-
26
- afterAll(async () => {
27
- await app.close();
28
- });
29
-
30
- it("[GET] /hello", async () => {
31
- const response = await request(app.getHttpServer())
32
- .get("/hello")
33
- .query({ show: true });
34
-
35
- expect(response.statusCode).toEqual(200);
36
- expect(response.body).toEqual({ message: "Hello world!" });
37
- });
38
- });
@@ -1,39 +0,0 @@
1
- import { DomainError } from "@/core/errors/domain-error";
2
- import { ValidationError } from "@/core/errors/validation.error";
3
- import { ErrorPresenter } from "@/infra/presenters/error.presenter";
4
- import {
5
- ArgumentsHost,
6
- BadRequestException,
7
- Catch,
8
- ExceptionFilter,
9
- HttpException,
10
- } from "@nestjs/common";
11
- import { FastifyReply } from "fastify";
12
- import { InternalServerError } from "../internal-server.error";
13
-
14
- @Catch(DomainError)
15
- export class DomainExceptionFilter implements ExceptionFilter {
16
- catch(exception: DomainError, host: ArgumentsHost): void {
17
- const ctx = host.switchToHttp();
18
- const response = ctx.getResponse<FastifyReply>();
19
-
20
- let httpException: HttpException;
21
-
22
- switch (exception.constructor) {
23
- case ValidationError:
24
- httpException = new BadRequestException(
25
- ErrorPresenter.toHttp(400, exception),
26
- );
27
- break;
28
-
29
- default:
30
- httpException = new InternalServerError(exception.message);
31
- }
32
-
33
- console.error(exception);
34
-
35
- response
36
- .status(httpException.getStatus())
37
- .send(httpException.getResponse());
38
- }
39
- }