@leo-h/create-nodejs-app 1.0.70 → 1.0.72

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leo-h/create-nodejs-app",
3
- "version": "1.0.70",
3
+ "version": "1.0.72",
4
4
  "packageManager": "pnpm@9.15.9",
5
5
  "author": "Leonardo Henrique <leonardo0507.business@gmail.com>",
6
6
  "description": "Create a modern Node.js app with TypeScript using one command.",
@@ -5,4 +5,4 @@ API_NAME=
5
5
  API_PORT=
6
6
 
7
7
  # example: "https://my-client-app.com" (url - only constraint | regex - specific constraint | "*" - not constraint)
8
- API_ACCESS_PERMISSION_CLIENT_SIDE=
8
+ API_CORS_ALLOW_ORIGIN=
@@ -0,0 +1,6 @@
1
+ # example: "app-name"
2
+ API_NAME="app-name"
3
+
4
+ # example: "3333"
5
+ API_PORT="3333"
6
+
@@ -8,7 +8,12 @@
8
8
  "indentStyle": "space"
9
9
  },
10
10
  "linter": {
11
- "enabled": true
11
+ "enabled": true,
12
+ "rules": {
13
+ "style": {
14
+ "noNonNullAssertion": "off"
15
+ }
16
+ }
12
17
  },
13
18
  "assist": {
14
19
  "enabled": false
@@ -3,8 +3,8 @@
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "scripts": {
6
- "start": "node --env-file=.env ./dist/src/http/server.js",
7
- "start:dev": "tsx watch --env-file=.env.development ./src/http/server.ts",
6
+ "start": "node ./dist/src/http/server.js",
7
+ "start:dev": "tsx watch ./src/http/server.ts",
8
8
  "typecheck": "tsc --noEmit",
9
9
  "lint": "biome lint",
10
10
  "lint:fix": "biome lint --write",
@@ -21,12 +21,13 @@
21
21
  "@elysiajs/cors": "1.4.0",
22
22
  "@elysiajs/node": "1.4.2",
23
23
  "@elysiajs/openapi": "1.4.11",
24
+ "dotenv": "17.2.3",
24
25
  "elysia": "1.4.16",
25
26
  "zod": "4.1.12"
26
27
  },
27
28
  "devDependencies": {
28
29
  "@biomejs/biome": "2.3.5",
29
- "@elysiajs/eden": "^1.4.4",
30
+ "@elysiajs/eden": "^1.4.5",
30
31
  "@faker-js/faker": "^10.1.0",
31
32
  "@swc/cli": "0.7.8",
32
33
  "@types/node": "24.9.1",
@@ -17,6 +17,9 @@ importers:
17
17
  '@elysiajs/openapi':
18
18
  specifier: 1.4.11
19
19
  version: 1.4.11(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))
20
+ dotenv:
21
+ specifier: 17.2.3
22
+ version: 17.2.3
20
23
  elysia:
21
24
  specifier: 1.4.16
22
25
  version: 1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3)
@@ -28,8 +31,8 @@ importers:
28
31
  specifier: 2.3.5
29
32
  version: 2.3.5
30
33
  '@elysiajs/eden':
31
- specifier: ^1.4.4
32
- version: 1.4.4(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))
34
+ specifier: ^1.4.5
35
+ version: 1.4.5(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))
33
36
  '@faker-js/faker':
34
37
  specifier: ^10.1.0
35
38
  version: 10.1.0
@@ -121,10 +124,10 @@ packages:
121
124
  peerDependencies:
122
125
  elysia: '>= 1.4.0'
123
126
 
124
- '@elysiajs/eden@1.4.4':
125
- resolution: {integrity: sha512-/LVqflmgUcCiXb8rz1iRq9Rx3SWfIV/EkoNqDFGMx+TvOyo8QHAygFXAVQz7RHs+jk6n6mEgpI6KlKBANoErsQ==}
127
+ '@elysiajs/eden@1.4.5':
128
+ resolution: {integrity: sha512-hIOeH+S5NU/84A7+t8yB1JjxqjmzRkBF9fnLn6y+AH8EcF39KumOAnciMhIOkhhThVZvXZ3d+GsizRc+Fxoi8g==}
126
129
  peerDependencies:
127
- elysia: '>= 1.4.0-exp.0'
130
+ elysia: '>= 1.4.0'
128
131
 
129
132
  '@elysiajs/node@1.4.2':
130
133
  resolution: {integrity: sha512-zqeBAV4/faCcmIEjCp3g6jRwsbaWsd5HqmlEf3CirD9HkTWQNo4T+GN/qGZi7zgd84D3Kzxsny7ZTMXEfrDSXQ==}
@@ -848,6 +851,10 @@ packages:
848
851
  resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
849
852
  engines: {node: '>=10'}
850
853
 
854
+ dotenv@17.2.3:
855
+ resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
856
+ engines: {node: '>=12'}
857
+
851
858
  eastasianwidth@0.2.0:
852
859
  resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
853
860
 
@@ -1466,7 +1473,7 @@ snapshots:
1466
1473
  dependencies:
1467
1474
  elysia: 1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3)
1468
1475
 
1469
- '@elysiajs/eden@1.4.4(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))':
1476
+ '@elysiajs/eden@1.4.5(elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3))':
1470
1477
  dependencies:
1471
1478
  elysia: 1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3)
1472
1479
 
@@ -2049,6 +2056,8 @@ snapshots:
2049
2056
 
2050
2057
  defer-to-connect@2.0.1: {}
2051
2058
 
2059
+ dotenv@17.2.3: {}
2060
+
2052
2061
  eastasianwidth@0.2.0: {}
2053
2062
 
2054
2063
  elysia@1.4.16(@sinclair/typebox@0.34.41)(exact-mirror@0.2.2(@sinclair/typebox@0.34.41))(file-type@20.5.0)(openapi-types@12.1.3)(typescript@5.9.3):
@@ -1,14 +1,48 @@
1
1
  import packageJson from "@/../package.json";
2
+ import { config } from "dotenv";
2
3
  import { existsSync, mkdirSync } from "node:fs";
3
4
  import { resolve } from "node:path";
4
5
  import { z } from "zod";
5
6
 
6
- const nodeEnv = ["production", "development", "test"] as const;
7
+ const envFilesForEachEnvironment = {
8
+ production: ".env",
9
+ development: ".env.development",
10
+ test: ".env.test",
11
+ } as const;
12
+
13
+ if (process.env?.npm_lifecycle_event === "start")
14
+ process.env.NODE_ENV = "production";
15
+
16
+ if (
17
+ [":dev", "dev:"].some((subcommand) =>
18
+ process.env.npm_lifecycle_event?.includes(subcommand),
19
+ )
20
+ )
21
+ process.env.NODE_ENV = "development";
22
+
23
+ if (
24
+ [":test", "test:"].some((subcommand) =>
25
+ process.env.npm_lifecycle_event?.includes(subcommand),
26
+ )
27
+ )
28
+ process.env.NODE_ENV = "test";
29
+
30
+ const nodeEnvEnum = ["production", "development", "test"] as const;
31
+ const nodeEnv = process.env.NODE_ENV as (typeof nodeEnvEnum)[number];
32
+ const envFilePath = resolve(process.cwd(), envFilesForEachEnvironment[nodeEnv]);
33
+
34
+ if (!existsSync(envFilePath))
35
+ throw new Error(
36
+ `Cannot find environment variables file in "${envFilePath}".`,
37
+ );
38
+
39
+ config({
40
+ path: envFilePath,
41
+ quiet: nodeEnv === "test",
42
+ });
7
43
 
8
44
  const schema = z.object({
9
- NODE_ENV: z
10
- .enum(nodeEnv)
11
- .default(process.env.NODE_ENV as (typeof nodeEnv)[number]),
45
+ NODE_ENV: z.enum(nodeEnvEnum).default(nodeEnv),
12
46
  API_NAME: z.string().default(packageJson.name),
13
47
  API_PORT: z.coerce.number().default(3333),
14
48
  API_CORS_ALLOW_ORIGIN: z.string().default("*"),
@@ -27,7 +61,7 @@ const schema = z.object({
27
61
  const parsedEnv = schema.safeParse(process.env);
28
62
 
29
63
  if (!parsedEnv.success) {
30
- console.error(parsedEnv.error.flatten().fieldErrors);
64
+ console.error(z.treeifyError(parsedEnv.error).properties);
31
65
 
32
66
  throw new Error("Invalid environment variables.");
33
67
  }
@@ -6,8 +6,7 @@ import openapi, { fromTypes } from "@elysiajs/openapi";
6
6
  import { Elysia } from "elysia";
7
7
  import z from "zod";
8
8
  import { controllers } from "./controllers";
9
- import "./controllers/hello.controller";
10
- import { globalErrorHandlerPlugin } from "./plugins/global-error-handler.plugin";
9
+ import { NotFoundError } from "./errors";
11
10
 
12
11
  export const openApiUrlPathname = "/openapi";
13
12
 
@@ -37,5 +36,8 @@ export const app = new Elysia({
37
36
  references: fromTypes(`.${__filename.replace(process.cwd(), "")}`),
38
37
  }),
39
38
  )
40
- .use(globalErrorHandlerPlugin.plugin())
39
+ .onError(({ code }) => {
40
+ if (code === "NOT_FOUND")
41
+ return new NotFoundError().setCode("NONEXISTENT_ROUTE").toController();
42
+ })
41
43
  .use(controllers);
@@ -1,7 +1,7 @@
1
1
  import { faker } from "@faker-js/faker";
2
2
  import { apiTestClient } from "test/integration/api-test-client";
3
3
  import { beforeEach, describe, expect, test } from "vitest";
4
- import { ValidationError } from "../errors";
4
+ import { UnprocessableEntityError } from "../errors";
5
5
  import { helloController } from "./hello.controller";
6
6
 
7
7
  const [route] = helloController.routes;
@@ -27,7 +27,7 @@ describe(`[Controller] ${route.method} ${route.path}`, () => {
27
27
 
28
28
  describe("should not be able to get hello world", () => {
29
29
  describe("if invalid input", () => {
30
- const error = new ValidationError();
30
+ const error = new UnprocessableEntityError().setCode("VALIDATION");
31
31
 
32
32
  test("with invalid property show", async () => {
33
33
  const result = await sut(defaultInput, {
@@ -54,9 +54,9 @@ describe(`[Controller] ${route.method} ${route.path}`, () => {
54
54
  },
55
55
  });
56
56
 
57
- const error = new ValidationError().setMessage(
58
- "Você não quer exibir o 'Hello world' :(",
59
- );
57
+ const error = new UnprocessableEntityError()
58
+ .setCode("VALIDATION")
59
+ .setMessage("Você não quer exibir o 'Hello world' :(");
60
60
 
61
61
  expect(result.status).toEqual(error.statusCode);
62
62
  expect(result.error).toMatchObject({ value: error.toSerialize() });
@@ -1,51 +1,66 @@
1
1
  import Elysia, { status } from "elysia";
2
2
  import z from "zod";
3
- import { ValidationError } from "../errors";
4
- import { globalErrorHandlerPlugin } from "../plugins/global-error-handler.plugin";
3
+ import { UnprocessableEntityError } from "../errors";
4
+ import { controllerErrorHandlerPlugin } from "../plugins/controller-error-handler.plugin";
5
5
 
6
- export const helloMultipartController = new Elysia().post(
7
- "/hello/multipart",
8
- ({ query, body }) => {
9
- const { show } = query;
10
- const { attachment } = body;
6
+ export const helloMultipartController = new Elysia()
7
+ .use(controllerErrorHandlerPlugin.plugin())
8
+ .post(
9
+ "/hello/multipart",
10
+ ({ query, body }) => {
11
+ const { show } = query;
12
+ const attachment = body.attachment as z.core.File;
11
13
 
12
- if (!show)
13
- return new ValidationError()
14
- .setMessage("Você não quer exibir o 'Hello world' :(")
15
- .setDebug(
16
- "Utilize o parâmetro de consulta 'show' com o valor 'true' para exibir o 'Hello world'.",
17
- )
18
- .toController();
14
+ if (!show)
15
+ return new UnprocessableEntityError()
16
+ .setCode("VALIDATION")
17
+ .setMessage("Você não quer exibir o 'Hello world' :(")
18
+ .setDebug(
19
+ "Utilize o parâmetro de consulta 'show' com o valor 'true' para exibir o 'Hello world'.",
20
+ )
21
+ .toController();
19
22
 
20
- return status(200, {
21
- message: "Hello world!",
22
- attachment: {
23
- name: attachment.name,
24
- mimetype: attachment.type,
25
- size: attachment.size,
26
- },
27
- });
28
- },
29
- {
30
- query: z.object({
31
- show: z
32
- .enum(["true", "false"])
33
- .transform<boolean>((val) => JSON.parse(val)),
34
- }),
35
- body: z.object({
36
- attachment: z
37
- .file()
38
- .max(1024 * 1024 * 50) // 50MB
39
- .mime("text/plain"),
40
- }),
41
- parse: "multipart/form-data",
42
- response: {
43
- ...globalErrorHandlerPlugin.getErrorSchemas(),
23
+ return status(200, {
24
+ message: "Hello world!",
25
+ attachment: {
26
+ name: attachment.name,
27
+ mimetype: attachment.type,
28
+ size: attachment.size,
29
+ },
30
+ });
44
31
  },
45
- detail: {
46
- operationId: "helloMultipartController",
47
- tags: ["Hello"],
48
- summary: "Hello world multipart!",
32
+ {
33
+ query: z.object({
34
+ show: z
35
+ .enum(["true", "false"])
36
+ .transform<boolean>((val) => JSON.parse(val)),
37
+ }),
38
+ body: z.object({
39
+ attachment: z
40
+ .file()
41
+ .max(1024 * 1024 * 50) // 50MB
42
+ .mime("text/plain")
43
+ // BUG(@elysiajs/openapi 1.4.11): Elysia stops generating the open API response with "fromTypes" function for the current route when using z.file()
44
+ // biome-ignore lint/suspicious/noExplicitAny: ↑
45
+ .transform((val) => val as any),
46
+ }),
47
+ response: {
48
+ // BUG(@elysiajs/openapi 1.4.11): Elysia is automatically inserting properties that it shouldn't for this class
49
+ 422: new UnprocessableEntityError()
50
+ .setCode("VALIDATION")
51
+ .toZodSchema()
52
+ .or(
53
+ new UnprocessableEntityError()
54
+ .setCode("VALIDATION")
55
+ .setMessage("Você não quer exibir o 'Hello world' :(")
56
+ .toZodSchema(),
57
+ ),
58
+ },
59
+ parse: "multipart/form-data",
60
+ detail: {
61
+ operationId: "helloMultipartController",
62
+ tags: ["Hello"],
63
+ summary: "Hello world multipart!",
64
+ },
49
65
  },
50
- },
51
- );
66
+ );
@@ -1,6 +1,6 @@
1
1
  import { apiTestClient } from "test/integration/api-test-client";
2
2
  import { describe, expect, test } from "vitest";
3
- import { ValidationError } from "../errors";
3
+ import { UnprocessableEntityError } from "../errors";
4
4
  import { helloController } from "./hello.controller";
5
5
 
6
6
  const [route] = helloController.routes;
@@ -9,7 +9,7 @@ const sut = apiTestClient.hello.get;
9
9
  describe(`[Controller] ${route.method} ${route.path}`, () => {
10
10
  describe("should not be able to get hello world", () => {
11
11
  describe("if invalid input", () => {
12
- const error = new ValidationError();
12
+ const error = new UnprocessableEntityError().setCode("VALIDATION");
13
13
 
14
14
  test("with invalid property show", async () => {
15
15
  const result = await sut({
@@ -36,9 +36,9 @@ describe(`[Controller] ${route.method} ${route.path}`, () => {
36
36
  },
37
37
  });
38
38
 
39
- const error = new ValidationError().setMessage(
40
- "Você não quer exibir o 'Hello world' :(",
41
- );
39
+ const error = new UnprocessableEntityError()
40
+ .setCode("VALIDATION")
41
+ .setMessage("Você não quer exibir o 'Hello world' :(");
42
42
 
43
43
  expect(result.status).toEqual(error.statusCode);
44
44
  expect(result.error).toMatchObject({ value: error.toSerialize() });
@@ -1,36 +1,36 @@
1
1
  import Elysia, { status } from "elysia";
2
2
  import z from "zod";
3
- import { ValidationError } from "../errors";
4
- import { globalErrorHandlerPlugin } from "../plugins/global-error-handler.plugin";
3
+ import { UnprocessableEntityError } from "../errors";
4
+ import { controllerErrorHandlerPlugin } from "../plugins/controller-error-handler.plugin";
5
5
 
6
- export const helloController = new Elysia().get(
7
- "/hello",
8
- ({ query }) => {
9
- const { show } = query;
6
+ export const helloController = new Elysia()
7
+ .use(controllerErrorHandlerPlugin.plugin())
8
+ .get(
9
+ "/hello",
10
+ ({ query }) => {
11
+ const { show } = query;
10
12
 
11
- if (!show)
12
- return new ValidationError()
13
- .setMessage("Você não quer exibir o 'Hello world' :(")
14
- .setDebug(
15
- "Utilize o parâmetro de consulta 'show' com o valor 'true' para exibir o 'Hello world'.",
16
- )
17
- .toController();
13
+ if (!show)
14
+ return new UnprocessableEntityError()
15
+ .setCode("VALIDATION")
16
+ .setMessage("Você não quer exibir o 'Hello world' :(")
17
+ .setDebug(
18
+ "Utilize o parâmetro de consulta 'show' com o valor 'true' para exibir o 'Hello world'.",
19
+ )
20
+ .toController();
18
21
 
19
- return status(200, { message: "Hello world!" });
20
- },
21
- {
22
- query: z.object({
23
- show: z
24
- .enum(["true", "false"])
25
- .transform<boolean>((val) => JSON.parse(val)),
26
- }),
27
- response: {
28
- ...globalErrorHandlerPlugin.getErrorSchemas(),
22
+ return status(200, { message: "Hello world!" });
29
23
  },
30
- detail: {
31
- operationId: "helloController",
32
- tags: ["Hello"],
33
- summary: "Hello world!",
24
+ {
25
+ query: z.object({
26
+ show: z
27
+ .enum(["true", "false"])
28
+ .transform<boolean>((val) => JSON.parse(val)),
29
+ }),
30
+ detail: {
31
+ operationId: "helloController",
32
+ tags: ["Hello"],
33
+ summary: "Hello world!",
34
+ },
34
35
  },
35
- },
36
- );
36
+ );
@@ -7,16 +7,23 @@ export abstract class BaseError<
7
7
  Name extends string = string,
8
8
  StatusCode extends number = number,
9
9
  Message extends string = string, // It cannot be undefined because of the native Error type.
10
+ Code extends string | null = null,
10
11
  Debug = undefined,
11
12
  > extends Error {
12
13
  public abstract name: Name;
13
14
  public abstract statusCode: StatusCode;
14
15
  public message!: Message;
16
+ public code!: Code;
15
17
  public debug!: Debug;
16
18
 
19
+ // biome-ignore lint/complexity/noUselessConstructor: this is necessary to prevent arguments when instantiating.
20
+ public constructor() {
21
+ super();
22
+ }
23
+
17
24
  private getUnbuildInstance<
18
- StatusCode extends number,
19
25
  Message extends string,
26
+ Code extends string | null,
20
27
  Debug,
21
28
  >() {
22
29
  return this as unknown as BaseError<
@@ -24,34 +31,34 @@ export abstract class BaseError<
24
31
  Name,
25
32
  StatusCode,
26
33
  Message,
34
+ Code,
27
35
  Debug
28
36
  >;
29
37
  }
30
38
 
31
- public setStatusCode<const StatusCodeInput extends number>(
32
- statusCode: StatusCodeInput,
33
- ) {
34
- this.statusCode = statusCode as unknown as StatusCode;
39
+ public setCode<const CodeInput extends string>(code: CodeInput) {
40
+ this.code = code as unknown as Code;
35
41
 
36
- return this.getUnbuildInstance<StatusCodeInput, Message, Debug>();
42
+ return this.getUnbuildInstance<Message, CodeInput, Debug>();
37
43
  }
38
44
 
39
45
  public setMessage<const MessageInput extends string>(message: MessageInput) {
40
46
  this.message = message as unknown as Message;
41
47
 
42
- return this.getUnbuildInstance<StatusCode, MessageInput, Debug>();
48
+ return this.getUnbuildInstance<MessageInput, Code, Debug>();
43
49
  }
44
50
 
45
51
  public setDebug<const DebugInput>(debug: DebugInput) {
46
52
  this.debug = debug as unknown as Debug;
47
53
 
48
- return this.getUnbuildInstance<StatusCode, Message, DebugInput>();
54
+ return this.getUnbuildInstance<Message, Code, DebugInput>();
49
55
  }
50
56
 
51
57
  public toSerialize() {
52
58
  return {
53
59
  name: this.name,
54
60
  statusCode: this.statusCode,
61
+ code: this.code,
55
62
  message: this.message,
56
63
  ...(env.NODE_ENV !== "production" &&
57
64
  this.debug && {
@@ -65,22 +72,32 @@ export abstract class BaseError<
65
72
  {
66
73
  name: Name;
67
74
  statusCode: StatusCode;
68
- message: string;
75
+ code: Code;
76
+ message: Message;
77
+ debug?: unknown;
69
78
  }
70
79
  > {
71
80
  return status(this.statusCode, this.toSerialize());
72
81
  }
73
82
 
74
- public toZodSchema(options?: { isMessageLiteral?: boolean }) {
75
- const { isMessageLiteral } = {
76
- isMessageLiteral: false,
77
- ...options,
83
+ public toZodSchema(
84
+ { nonLiteral }: { nonLiteral: ("code" | "message")[] } = { nonLiteral: [] },
85
+ ) {
86
+ const getCodeSchema = () => {
87
+ if (nonLiteral.includes("code")) return z.string();
88
+
89
+ if (this.code) return z.literal(this.code);
90
+
91
+ return z.null();
78
92
  };
79
93
 
80
94
  return z.object({
81
95
  name: z.literal(this.name),
82
96
  statusCode: z.literal(this.statusCode),
83
- message: isMessageLiteral ? z.literal(this.message) : z.string(),
97
+ code: getCodeSchema(),
98
+ message: nonLiteral.includes("message")
99
+ ? z.string()
100
+ : z.literal(this.message),
84
101
  });
85
102
  }
86
103
  }
@@ -101,32 +118,86 @@ export class InternalServerError extends BaseError<
101
118
  public readonly message = InternalServerError.message;
102
119
  }
103
120
 
104
- export class ValidationError extends BaseError<
105
- ValidationError,
106
- typeof ValidationError.name,
107
- typeof ValidationError.statusCode,
108
- typeof ValidationError.message
121
+ export class BadRequestError extends BaseError<
122
+ BadRequestError,
123
+ typeof BadRequestError.name,
124
+ typeof BadRequestError.statusCode
109
125
  > {
110
- public static readonly name = "VALIDATION_ERROR";
126
+ public static readonly name = "BAD_REQUEST_ERROR";
127
+ public static readonly statusCode = 400;
128
+
129
+ public readonly name = BadRequestError.name;
130
+ public readonly statusCode = BadRequestError.statusCode;
131
+ }
132
+
133
+ export class UnprocessableEntityError extends BaseError<
134
+ UnprocessableEntityError,
135
+ typeof UnprocessableEntityError.name,
136
+ typeof UnprocessableEntityError.statusCode,
137
+ typeof UnprocessableEntityError.message
138
+ > {
139
+ public static readonly name = "UNPROCESSABLE_ENTITY";
111
140
  public static readonly statusCode = 422;
112
141
  public static readonly message = "Os dados enviados são inválidos.";
113
142
 
114
- public readonly name = ValidationError.name;
115
- public readonly statusCode = ValidationError.statusCode;
116
- public readonly message = ValidationError.message;
143
+ public readonly name = UnprocessableEntityError.name;
144
+ public readonly statusCode = UnprocessableEntityError.statusCode;
145
+ public readonly message = UnprocessableEntityError.message;
117
146
  }
118
147
 
119
- export class ResourceNotFoundError extends BaseError<
120
- ResourceNotFoundError,
121
- typeof ResourceNotFoundError.name,
122
- typeof ResourceNotFoundError.statusCode,
123
- typeof ResourceNotFoundError.message
148
+ export class NotFoundError extends BaseError<
149
+ NotFoundError,
150
+ typeof NotFoundError.name,
151
+ typeof NotFoundError.statusCode,
152
+ typeof NotFoundError.message
124
153
  > {
125
- public static readonly name = "RESOURCE_NOT_FOUND_ERROR";
154
+ public static readonly name = "NOT_FOUND";
126
155
  public static readonly statusCode = 404;
127
156
  public static readonly message = "Recurso não encontrado.";
128
157
 
129
- public readonly name = ResourceNotFoundError.name;
130
- public readonly statusCode = ResourceNotFoundError.statusCode;
131
- public readonly message = ResourceNotFoundError.message;
158
+ public readonly name = NotFoundError.name;
159
+ public readonly statusCode = NotFoundError.statusCode;
160
+ public readonly message = NotFoundError.message;
161
+ }
162
+
163
+ export class ConflictError extends BaseError<
164
+ ConflictError,
165
+ typeof ConflictError.name,
166
+ typeof ConflictError.statusCode
167
+ > {
168
+ public static readonly name = "CONFLICT";
169
+ public static readonly statusCode = 409;
170
+
171
+ public readonly name = ConflictError.name;
172
+ public readonly statusCode = ConflictError.statusCode;
173
+ }
174
+
175
+ export class ForbiddenError extends BaseError<
176
+ ForbiddenError,
177
+ typeof ForbiddenError.name,
178
+ typeof ForbiddenError.statusCode,
179
+ typeof ForbiddenError.message
180
+ > {
181
+ public static readonly name = "FORBIDDEN";
182
+ public static readonly statusCode = 403;
183
+ public static readonly message = "Acesso negado.";
184
+
185
+ public readonly name = ForbiddenError.name;
186
+ public readonly statusCode = ForbiddenError.statusCode;
187
+ public readonly message = ForbiddenError.message;
188
+ }
189
+
190
+ export class UnauthorizedError extends BaseError<
191
+ UnauthorizedError,
192
+ typeof UnauthorizedError.name,
193
+ typeof UnauthorizedError.statusCode,
194
+ typeof UnauthorizedError.message
195
+ > {
196
+ public static readonly name = "UNAUTHORIZED";
197
+ public static readonly statusCode = 401;
198
+ public static readonly message = "Não autorizado.";
199
+
200
+ public readonly name = UnauthorizedError.name;
201
+ public readonly statusCode = UnauthorizedError.statusCode;
202
+ public readonly message = UnauthorizedError.message;
132
203
  }
@@ -1,34 +1,20 @@
1
- import type { ZodRestrictFieldsShape } from "@/@types/zod-utils";
2
1
  import Elysia, {
3
2
  InternalServerError as ElysiaInternalServerError,
4
3
  } from "elysia";
5
4
  import {
5
+ BadRequestError,
6
6
  InternalServerError,
7
- ResourceNotFoundError,
8
- ValidationError,
7
+ UnprocessableEntityError,
9
8
  } from "../errors";
10
9
 
11
- type MetadataResponse = ReturnType<typeof plugin> extends Elysia<
12
- // biome-ignore-start lint/suspicious/noExplicitAny: unused generics
13
- any,
14
- any,
15
- any,
16
- // biome-ignore-end lint/suspicious/noExplicitAny: unused generics
17
- infer Metadata
18
- >
19
- ? Metadata["response"]
20
- : never;
21
-
22
10
  function plugin() {
23
- return new Elysia({ name: "global-error-handler-plugin" }).onError(
24
- { as: "global" },
11
+ return new Elysia({ name: "controller-error-handler-plugin" }).onError(
12
+ { as: "scoped" },
25
13
  ({ code, error }) => {
26
14
  switch (code) {
27
- case "NOT_FOUND": {
28
- return new ResourceNotFoundError().toController();
29
- }
30
15
  case "VALIDATION": {
31
- return new ValidationError()
16
+ return new UnprocessableEntityError()
17
+ .setCode("VALIDATION")
32
18
  .setDebug({
33
19
  elysiaCode: code,
34
20
  name: error.name,
@@ -40,8 +26,9 @@ function plugin() {
40
26
  case "PARSE":
41
27
  case "INVALID_COOKIE_SIGNATURE":
42
28
  case "INVALID_FILE_TYPE":
43
- return new ValidationError()
44
- .setStatusCode(400)
29
+ return new BadRequestError()
30
+ .setCode("VALIDATION")
31
+ .setMessage(UnprocessableEntityError.message)
45
32
  .setDebug({
46
33
  elysiaCode: code,
47
34
  name: error.name,
@@ -68,15 +55,6 @@ function plugin() {
68
55
  );
69
56
  }
70
57
 
71
- function getErrorSchemas() {
72
- return {
73
- "422": new ValidationError().toZodSchema(),
74
- "400": new ValidationError().setStatusCode(400).toZodSchema(),
75
- "500": new InternalServerError().toZodSchema(),
76
- } satisfies Omit<ZodRestrictFieldsShape<MetadataResponse>, 404>;
77
- }
78
-
79
- export const globalErrorHandlerPlugin = Object.freeze({
58
+ export const controllerErrorHandlerPlugin = Object.freeze({
80
59
  plugin,
81
- getErrorSchemas,
82
60
  });
@@ -4,6 +4,9 @@ import { defineConfig } from "vitest/config";
4
4
  export default defineConfig({
5
5
  plugins: [viteTsconfigPaths()],
6
6
  test: {
7
+ env: {
8
+ NODE_ENV: "test",
9
+ },
7
10
  fakeTimers: {
8
11
  toFake: [
9
12
  "Date",
@@ -1,3 +0,0 @@
1
- API_NAME="app-name-development"
2
- API_PORT="3333"
3
- API_ACCESS_PERMISSION_CLIENT_SIDE="https://my-client-app.com"