@zentjs/zentjs 0.0.2
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/LICENSE +28 -0
- package/README.md +1584 -0
- package/package.json +85 -0
- package/src/core/application.mjs +1039 -0
- package/src/core/context.mjs +33 -0
- package/src/errors/error-handler.mjs +88 -0
- package/src/errors/http-error.mjs +86 -0
- package/src/hooks/lifecycle.mjs +175 -0
- package/src/http/request.mjs +166 -0
- package/src/http/response.mjs +151 -0
- package/src/index.mjs +33 -0
- package/src/middleware/pipeline.mjs +77 -0
- package/src/plugins/body-parser.mjs +165 -0
- package/src/plugins/cors.mjs +183 -0
- package/src/plugins/manager.mjs +112 -0
- package/src/plugins/request-metrics.mjs +74 -0
- package/src/router/index.mjs +198 -0
- package/src/router/node.mjs +72 -0
- package/src/router/radix-tree.mjs +313 -0
- package/src/utils/http-status.mjs +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,1584 @@
|
|
|
1
|
+
# ZentJS
|
|
2
|
+
|
|
3
|
+
[](https://github.com/walber-vaz/zentjs/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/@zentjs/zentjs)
|
|
5
|
+
[](https://www.npmjs.com/package/@zentjs/zentjs)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
> Framework web minimalista e performático para Node.js, inspirado no Express e Fastify.
|
|
9
|
+
|
|
10
|
+
**Zero dependências em runtime** · **ESM-only** · **Node.js ≥ 24**
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Sumário
|
|
15
|
+
|
|
16
|
+
- [Visão Geral](#visão-geral)
|
|
17
|
+
- [Plano de Documentação](#plano-de-documentação)
|
|
18
|
+
- [Estado da Documentação](#estado-da-documentação)
|
|
19
|
+
- [Padrão Editorial](#padrão-editorial)
|
|
20
|
+
- [Glossário de Termos](#glossário-de-termos)
|
|
21
|
+
- [Motivação e Princípios](#motivação-e-princípios)
|
|
22
|
+
- [Arquitetura Geral](#arquitetura-geral)
|
|
23
|
+
- [Estrutura de Diretórios](#estrutura-de-diretórios)
|
|
24
|
+
- [Componentes Principais](#componentes-principais)
|
|
25
|
+
- [Application (Zent)](#1-application-zent)
|
|
26
|
+
- [Router (Radix Tree)](#2-router-radix-tree)
|
|
27
|
+
- [Request](#3-request)
|
|
28
|
+
- [Response](#4-response)
|
|
29
|
+
- [Middleware Pipeline](#5-middleware-pipeline)
|
|
30
|
+
- [Plugin System](#6-plugin-system)
|
|
31
|
+
- [Lifecycle Hooks](#7-lifecycle-hooks)
|
|
32
|
+
- [Context (ctx)](#8-context-ctx)
|
|
33
|
+
- [Error Handling](#9-error-handling)
|
|
34
|
+
- [Fluxo de uma Requisição](#fluxo-de-uma-requisição)
|
|
35
|
+
- [API Pública](#api-pública)
|
|
36
|
+
- [Route Groups](#route-groups)
|
|
37
|
+
- [Exemplos de Uso](#exemplos-de-uso)
|
|
38
|
+
- [Guia: Primeira API (CRUD básico)](#guia-primeira-api-crud-básico)
|
|
39
|
+
- [Guia: Autenticação por plugin](#guia-autenticação-por-plugin)
|
|
40
|
+
- [Guia: Métricas com requestMetrics](#guia-métricas-com-requestmetrics)
|
|
41
|
+
- [Guia: Testes com inject + Vitest](#guia-testes-com-inject--vitest)
|
|
42
|
+
- [Roadmap de Implementação](#roadmap-de-implementação)
|
|
43
|
+
- [Decisões Técnicas (ADRs)](#decisões-técnicas-adrs)
|
|
44
|
+
- [Contribuição](#contribuição)
|
|
45
|
+
- [Referências](#referências)
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Plano de Documentação
|
|
50
|
+
|
|
51
|
+
A evolução da documentação agora segue etapas incrementais com checklist versionado.
|
|
52
|
+
|
|
53
|
+
- Arquivo de acompanhamento: [docs/DOCUMENTATION_TODO.md](docs/DOCUMENTATION_TODO.md)
|
|
54
|
+
- Status atual: **Etapa 6 concluída**
|
|
55
|
+
- Próxima etapa: manutenção contínua da documentação por PR
|
|
56
|
+
|
|
57
|
+
> Regra de ouro: documentar apenas comportamento já implementado no runtime.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Estado da Documentação
|
|
62
|
+
|
|
63
|
+
Este README é a referência principal do projeto e segue atualização incremental por etapas.
|
|
64
|
+
|
|
65
|
+
- Fonte de controle: [docs/DOCUMENTATION_TODO.md](docs/DOCUMENTATION_TODO.md)
|
|
66
|
+
- Escopo atual: consistência final entre runtime, README e exemplos
|
|
67
|
+
- Política: exemplos e contratos devem refletir o runtime real em `src/`
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Padrão Editorial
|
|
72
|
+
|
|
73
|
+
Para manter consistência entre seções, usar o padrão abaixo:
|
|
74
|
+
|
|
75
|
+
- **API pública**: assinatura + parâmetros + retorno + exemplo mínimo executável
|
|
76
|
+
- **Fluxos**: ordem de execução explícita (hooks, middlewares, handler)
|
|
77
|
+
- **Erros**: classe/condição + status HTTP + formato de resposta
|
|
78
|
+
- **Exemplos**: preferir snippets curtos, com foco em um conceito por bloco
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Glossário de Termos
|
|
83
|
+
|
|
84
|
+
- **Handler**: função final da rota que produz o resultado da requisição
|
|
85
|
+
- **Middleware**: função `async (ctx, next)` no pipeline (antes/depois do handler)
|
|
86
|
+
- **Hook**: interceptador de fase específica do lifecycle (`onRequest`, `preHandler`, etc.)
|
|
87
|
+
- **Plugin Scope**: escopo encapsulado criado por `register()`, com herança controlada
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Visão Geral
|
|
92
|
+
|
|
93
|
+
**ZentJS** é uma framework HTTP para construção de APIs e aplicações web em Node.js.
|
|
94
|
+
O objetivo é combinar o melhor dos dois mundos:
|
|
95
|
+
|
|
96
|
+
| Inspiração | O que trazemos |
|
|
97
|
+
| ----------- | -------------------------------------------------------------------------- |
|
|
98
|
+
| **Express** | API simples e intuitiva, middleware `(req, res, next)` |
|
|
99
|
+
| **Fastify** | Performance, sistema de plugins com encapsulamento, hooks de ciclo de vida |
|
|
100
|
+
|
|
101
|
+
O resultado é uma framework leve, sem dependências de runtime, construída 100% sobre o módulo nativo `node:http`.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Motivação e Princípios
|
|
106
|
+
|
|
107
|
+
### Por que criar outra framework?
|
|
108
|
+
|
|
109
|
+
1. **Aprendizado profundo** — Entender os internos de uma framework HTTP modular.
|
|
110
|
+
2. **Zero dependências** — O core não depende de nenhum pacote externo.
|
|
111
|
+
3. **ESM nativo** — Sem CommonJS, sem transpilação, sem build step.
|
|
112
|
+
4. **Performance by design** — Roteamento via Radix Tree, sem regex em hot path.
|
|
113
|
+
5. **Developer Experience** — API clara, erros descritivos, tipagem via JSDoc.
|
|
114
|
+
|
|
115
|
+
### Princípios Arquiteturais
|
|
116
|
+
|
|
117
|
+
| Princípio | Descrição |
|
|
118
|
+
| -------------------------- | ---------------------------------------------------------- |
|
|
119
|
+
| **Single Responsibility** | Cada módulo tem uma única razão para mudar |
|
|
120
|
+
| **Open/Closed** | Extensível via plugins, fechado para modificação no core |
|
|
121
|
+
| **Composição > Herança** | Plugins e middlewares compõem funcionalidade |
|
|
122
|
+
| **Fail Fast** | Erros são detectados e reportados o mais cedo possível |
|
|
123
|
+
| **Convention over Config** | Defaults sensatos, mas tudo configurável |
|
|
124
|
+
| **Immutable por padrão** | Objetos de configuração não são mutados após inicialização |
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## Arquitetura Geral
|
|
129
|
+
|
|
130
|
+
```text
|
|
131
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
132
|
+
│ ZentJS Core │
|
|
133
|
+
│ │
|
|
134
|
+
│ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
|
|
135
|
+
│ │ Server │───▶│ Router │───▶│ Middleware│───▶│ Handler │ │
|
|
136
|
+
│ │ (node:http)│ │(RadixTree)│ │ Pipeline │ │ (user fn) │ │
|
|
137
|
+
│ └────────────┘ └───────────┘ └───────────┘ └───────────┘ │
|
|
138
|
+
│ │ │ │
|
|
139
|
+
│ ▼ ▼ │
|
|
140
|
+
│ ┌───────────┐ ┌────────────┐ │
|
|
141
|
+
│ │ Request │ │ Response │ │
|
|
142
|
+
│ │ (wrapper) │◄──────── Context (ctx) ───────────▶│ (wrapper) │ │
|
|
143
|
+
│ └───────────┘ └────────────┘ │
|
|
144
|
+
│ │
|
|
145
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
146
|
+
│ │ Lifecycle Hooks │ │
|
|
147
|
+
│ │ onRequest → preParsing → preValidation → preHandler │ │
|
|
148
|
+
│ │ → onSend → onResponse → onError │ │
|
|
149
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
150
|
+
│ │
|
|
151
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
152
|
+
│ │ Plugin System │ │
|
|
153
|
+
│ │ register() → encapsulated scope → decorators │ │
|
|
154
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
155
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Estrutura de Diretórios
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
zentjs/
|
|
164
|
+
├── src/
|
|
165
|
+
│ ├── index.mjs # Entry point — exporta a função zent()
|
|
166
|
+
│ │
|
|
167
|
+
│ ├── core/
|
|
168
|
+
│ │ ├── application.mjs # Classe Zent (instância do app)
|
|
169
|
+
│ │ └── context.mjs # Objeto de contexto por requisição
|
|
170
|
+
│ │
|
|
171
|
+
│ ├── http/
|
|
172
|
+
│ │ ├── request.mjs # Wrapper do IncomingMessage
|
|
173
|
+
│ │ └── response.mjs # Wrapper do ServerResponse
|
|
174
|
+
│ │
|
|
175
|
+
│ ├── router/
|
|
176
|
+
│ │ ├── index.mjs # Router público
|
|
177
|
+
│ │ ├── node.mjs # Nó da árvore (estático, param, wildcard)
|
|
178
|
+
│ │ ├── radix-tree.mjs # Implementação da Radix Tree
|
|
179
|
+
│ │
|
|
180
|
+
│ ├── middleware/
|
|
181
|
+
│ │ └── pipeline.mjs # Executor da cadeia de middlewares
|
|
182
|
+
│ │
|
|
183
|
+
│ ├── plugins/
|
|
184
|
+
│ │ ├── manager.mjs # Registro e carregamento de plugins
|
|
185
|
+
│ │ ├── body-parser.mjs # Parser de body (JSON, URL-encoded, text)
|
|
186
|
+
│ │ └── cors.mjs # Middleware CORS built-in
|
|
187
|
+
│ │
|
|
188
|
+
│ ├── hooks/
|
|
189
|
+
│ │ └── lifecycle.mjs # Gerenciador dos lifecycle hooks
|
|
190
|
+
│ │
|
|
191
|
+
│ └── errors/
|
|
192
|
+
│ ├── http-error.mjs # Classe base HttpError
|
|
193
|
+
│ └── error-handler.mjs # Handler global de erros
|
|
194
|
+
│
|
|
195
|
+
├── test/
|
|
196
|
+
│ ├── setupTests.mjs
|
|
197
|
+
│ ├── unit/
|
|
198
|
+
│ │ └── ... (13 arquivos de testes unitários)
|
|
199
|
+
│ └── integration/
|
|
200
|
+
│ └── plugins-http.integration.test.mjs
|
|
201
|
+
│
|
|
202
|
+
├── examples/
|
|
203
|
+
│ ├── hello-world.mjs
|
|
204
|
+
│ ├── rest-api.mjs
|
|
205
|
+
│ └── with-plugins.mjs
|
|
206
|
+
│
|
|
207
|
+
├── package.json
|
|
208
|
+
├── vitest.config.mjs
|
|
209
|
+
├── eslint.config.mjs
|
|
210
|
+
└── README.md
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Componentes Principais
|
|
216
|
+
|
|
217
|
+
### 1. Application (Zent)
|
|
218
|
+
|
|
219
|
+
O ponto de entrada da framework. Cria e configura a instância do servidor.
|
|
220
|
+
|
|
221
|
+
**Arquivo:** `src/core/application.mjs`
|
|
222
|
+
|
|
223
|
+
**Responsabilidades:**
|
|
224
|
+
|
|
225
|
+
- Inicializar o servidor HTTP
|
|
226
|
+
- Registrar rotas (proxy para o Router)
|
|
227
|
+
- Registrar middlewares globais
|
|
228
|
+
- Registrar plugins
|
|
229
|
+
- Gerenciar lifecycle hooks
|
|
230
|
+
- Iniciar/parar o servidor (`listen` / `close`)
|
|
231
|
+
|
|
232
|
+
**Interface:**
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
import { zent } from 'zentjs';
|
|
236
|
+
|
|
237
|
+
const app = zent({
|
|
238
|
+
// Opções de configuração
|
|
239
|
+
ignoreTrailingSlash: true, // /users e /users/ são a mesma rota
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Registrar rotas
|
|
243
|
+
app.get('/hello', (ctx) => {
|
|
244
|
+
return ctx.res.json({ message: 'Hello, World!' });
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Iniciar servidor
|
|
248
|
+
app.listen({ port: 3000, host: '0.0.0.0' }, (err, address) => {
|
|
249
|
+
if (err) throw err;
|
|
250
|
+
console.log(`Server listening on ${address}`);
|
|
251
|
+
});
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Diagrama de classe:**
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
┌────────────────────────────────────┐
|
|
258
|
+
│ Zent (Application) │
|
|
259
|
+
├────────────────────────────────────┤
|
|
260
|
+
│ - _server: HttpServer │
|
|
261
|
+
│ - _router: Router │
|
|
262
|
+
│ - _plugins: PluginManager │
|
|
263
|
+
│ - _hooks: LifecycleManager │
|
|
264
|
+
│ - _middlewares: Middleware[] │
|
|
265
|
+
│ - _options: ZentOptions │
|
|
266
|
+
├────────────────────────────────────┤
|
|
267
|
+
│ + get(path, opts?, handler) │
|
|
268
|
+
│ + post(path, opts?, handler) │
|
|
269
|
+
│ + put(path, opts?, handler) │
|
|
270
|
+
│ + patch(path, opts?, handler) │
|
|
271
|
+
│ + delete(path, opts?, handler) │
|
|
272
|
+
│ + head(path, opts?, handler) │
|
|
273
|
+
│ + options(path, opts?, handler) │
|
|
274
|
+
│ + all(path, opts?, handler) │
|
|
275
|
+
│ + use(middleware) │
|
|
276
|
+
│ + register(plugin, opts?) │
|
|
277
|
+
│ + addHook(name, fn) │
|
|
278
|
+
│ + decorate(name, value) │
|
|
279
|
+
│ + listen(opts, callback?) │
|
|
280
|
+
│ + close() │
|
|
281
|
+
│ + inject(opts): Promise<Response> │
|
|
282
|
+
└────────────────────────────────────┘
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
> **`inject()`** permite testar rotas sem abrir uma porta de rede (inspirado no Fastify).
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### 2. Router (Radix Tree)
|
|
290
|
+
|
|
291
|
+
Roteamento de alta performance usando uma **Radix Tree** (também chamada Patricia Trie ou Compact Prefix Tree).
|
|
292
|
+
|
|
293
|
+
**Arquivo:** `src/router/radix-tree.mjs`
|
|
294
|
+
|
|
295
|
+
**Por que Radix Tree?**
|
|
296
|
+
|
|
297
|
+
| Abordagem | Complexidade (lookup) | Usado por |
|
|
298
|
+
| -------------- | --------------------- | ----------------------- |
|
|
299
|
+
| Array linear | O(n) | Express |
|
|
300
|
+
| Regex matching | O(n) | Koa |
|
|
301
|
+
| **Radix Tree** | **O(k)** \* | **Fastify**, **ZentJS** |
|
|
302
|
+
|
|
303
|
+
_\* k = comprimento do path, independente do número de rotas_
|
|
304
|
+
|
|
305
|
+
**Funcionalidades:**
|
|
306
|
+
|
|
307
|
+
- Rotas estáticas: `/users/list`
|
|
308
|
+
- Parâmetros nomeados: `/users/:id`
|
|
309
|
+
- Wildcard: `/static/*filepath`
|
|
310
|
+
- Suporte a múltiplos métodos HTTP por path
|
|
311
|
+
|
|
312
|
+
**Estrutura do nó:**
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
// Cada nó na Radix Tree
|
|
316
|
+
{
|
|
317
|
+
prefix: '/users', // Fragmento do path
|
|
318
|
+
children: Map {}, // Filhos indexados pelo primeiro caractere
|
|
319
|
+
paramChild: null, // Filho de parâmetro (:param)
|
|
320
|
+
wildcardChild: null, // Filho wildcard (*)
|
|
321
|
+
handlers: Map { // Handlers por método HTTP
|
|
322
|
+
'GET': { handler, hooks, middlewares },
|
|
323
|
+
'POST': { handler, hooks, middlewares }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Exemplo de árvore para as rotas:**
|
|
329
|
+
|
|
330
|
+
```text
|
|
331
|
+
GET /users
|
|
332
|
+
GET /users/:id
|
|
333
|
+
POST /users
|
|
334
|
+
GET /users/:id/posts
|
|
335
|
+
GET /about
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
```text
|
|
339
|
+
root ('')
|
|
340
|
+
├── /u
|
|
341
|
+
│ └── sers
|
|
342
|
+
│ ├── [GET, POST handlers]
|
|
343
|
+
│ └── /:id
|
|
344
|
+
│ ├── [GET handler]
|
|
345
|
+
│ └── /posts
|
|
346
|
+
│ └── [GET handler]
|
|
347
|
+
└── /about
|
|
348
|
+
└── [GET handler]
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**API do Router:**
|
|
352
|
+
|
|
353
|
+
```js
|
|
354
|
+
class Router {
|
|
355
|
+
add(method, path, handler, opts?) // Adiciona rota
|
|
356
|
+
find(method, path) // Busca rota → { handler, params, hooks }
|
|
357
|
+
all(path, handler) // Registra para todos os métodos HTTP
|
|
358
|
+
group(prefix, opts?, callback) // Agrupa rotas sob prefixo
|
|
359
|
+
}
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Route Groups:**
|
|
363
|
+
|
|
364
|
+
```js
|
|
365
|
+
// Grupo com prefixo + middlewares compartilhados
|
|
366
|
+
router.group('/api', { middlewares: [auth] }, (group) => {
|
|
367
|
+
group.get('/users', listUsers); // GET /api/users
|
|
368
|
+
group.post('/users', createUser); // POST /api/users
|
|
369
|
+
|
|
370
|
+
// Sub-grupo aninhado — herda middlewares do pai
|
|
371
|
+
group.group('/admin', { middlewares: [adminOnly] }, (admin) => {
|
|
372
|
+
admin.delete('/users/:id', deleteUser); // DELETE /api/admin/users/:id
|
|
373
|
+
// middlewares executados: [auth, adminOnly]
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### 3. Request
|
|
381
|
+
|
|
382
|
+
Wrapper sobre `http.IncomingMessage` que fornece uma API mais ergonômica.
|
|
383
|
+
|
|
384
|
+
**Arquivo:** `src/http/request.mjs`
|
|
385
|
+
|
|
386
|
+
**Propriedades e métodos:**
|
|
387
|
+
|
|
388
|
+
```js
|
|
389
|
+
class ZentRequest {
|
|
390
|
+
// Propriedades parseadas do request original
|
|
391
|
+
get method() // 'GET', 'POST', etc.
|
|
392
|
+
get url() // URL completa
|
|
393
|
+
get path() // Path sem query string
|
|
394
|
+
get query() // Query params como objeto { key: value }
|
|
395
|
+
get headers() // Headers como objeto
|
|
396
|
+
get params() // Route params { id: '123' }
|
|
397
|
+
get ip() // IP do cliente
|
|
398
|
+
get hostname() // Hostname da requisição
|
|
399
|
+
get protocol() // 'http' ou 'https'
|
|
400
|
+
|
|
401
|
+
// Body (populado após parsing)
|
|
402
|
+
get body() // Body parseado (JSON, form, etc.)
|
|
403
|
+
|
|
404
|
+
// Helpers
|
|
405
|
+
is(type) // Verifica Content-Type
|
|
406
|
+
get(header) // Retorna valor de um header
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
**Decisão:** O body **não** é parseado automaticamente. O usuário deve usar o middleware `bodyParser()` ou ler manualmente. Isso garante zero overhead para rotas que não precisam de body.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
### 4. Response
|
|
415
|
+
|
|
416
|
+
Wrapper sobre `http.ServerResponse` com API fluente (chainable).
|
|
417
|
+
|
|
418
|
+
**Arquivo:** `src/http/response.mjs`
|
|
419
|
+
|
|
420
|
+
**API:**
|
|
421
|
+
|
|
422
|
+
```js
|
|
423
|
+
class ZentResponse {
|
|
424
|
+
// Status
|
|
425
|
+
status(code) // Define status code → retorna this
|
|
426
|
+
|
|
427
|
+
// Headers
|
|
428
|
+
header(name, value) // Define header → retorna this
|
|
429
|
+
type(contentType) // Atalho para Content-Type → retorna this
|
|
430
|
+
|
|
431
|
+
// Envio de resposta
|
|
432
|
+
json(data) // Serializa como JSON e envia
|
|
433
|
+
send(data) // Envia string/Buffer
|
|
434
|
+
html(data) // Envia como text/html
|
|
435
|
+
redirect(url, code?) // Redireciona (default 302)
|
|
436
|
+
empty(code?) // Resposta sem body (default 204)
|
|
437
|
+
|
|
438
|
+
// Propriedades
|
|
439
|
+
get sent() // Boolean: response já foi enviada?
|
|
440
|
+
get statusCode() // Status code atual
|
|
441
|
+
}
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Exemplo de uso:**
|
|
445
|
+
|
|
446
|
+
```js
|
|
447
|
+
app.get('/users/:id', (ctx) => {
|
|
448
|
+
const user = findUser(ctx.req.params.id);
|
|
449
|
+
|
|
450
|
+
if (!user) {
|
|
451
|
+
return ctx.res.status(404).json({ error: 'User not found' });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
return ctx.res.json(user);
|
|
455
|
+
});
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
### 5. Middleware Pipeline
|
|
461
|
+
|
|
462
|
+
Sistema de middlewares inspirado no Express, mas com execução baseada em `async/await`.
|
|
463
|
+
|
|
464
|
+
**Arquivo:** `src/middleware/pipeline.mjs`
|
|
465
|
+
|
|
466
|
+
**Signature do middleware:**
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
// Middleware com next()
|
|
470
|
+
async function myMiddleware(ctx, next) {
|
|
471
|
+
// Antes do handler
|
|
472
|
+
console.log('Before');
|
|
473
|
+
|
|
474
|
+
await next();
|
|
475
|
+
|
|
476
|
+
// Depois do handler (response já preparada)
|
|
477
|
+
console.log('After');
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
**Tipos de middleware:**
|
|
482
|
+
|
|
483
|
+
```text
|
|
484
|
+
┌──────────────────────────────────────────────────┐
|
|
485
|
+
│ Middleware Pipeline │
|
|
486
|
+
│ │
|
|
487
|
+
│ 1. Global Middlewares app.use(fn) │
|
|
488
|
+
│ 2. Route-level Middlewares route opts │
|
|
489
|
+
│ 3. Plugin-scoped dentro de plugins │
|
|
490
|
+
│ │
|
|
491
|
+
│ Execução: Onion Model (como Koa) │
|
|
492
|
+
│ │
|
|
493
|
+
│ ┌───────────────────────────────────────────┐ │
|
|
494
|
+
│ │ Middleware 1 (before) │ │
|
|
495
|
+
│ │ ┌───────────────────────────────────┐ │ │
|
|
496
|
+
│ │ │ Middleware 2 (before) │ │ │
|
|
497
|
+
│ │ │ ┌──────────────────────────┐ │ │ │
|
|
498
|
+
│ │ │ │ Route Handler │ │ │ │
|
|
499
|
+
│ │ │ └──────────────────────────┘ │ │ │
|
|
500
|
+
│ │ │ Middleware 2 (after) │ │ │
|
|
501
|
+
│ │ └───────────────────────────────────┘ │ │
|
|
502
|
+
│ │ Middleware 1 (after) │ │
|
|
503
|
+
│ └───────────────────────────────────────────┘ │
|
|
504
|
+
└──────────────────────────────────────────────────┘
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
**Assinaturas suportadas na aplicação:**
|
|
508
|
+
|
|
509
|
+
- `app.use(fn)` — middleware global
|
|
510
|
+
- `app.use('/prefix', fn)` — middleware global com prefix match
|
|
511
|
+
- `route options.middlewares` — middleware por rota
|
|
512
|
+
- `group(..., { middlewares })` — middleware herdado por grupo/subgrupo
|
|
513
|
+
|
|
514
|
+
**Ordem de execução efetiva:**
|
|
515
|
+
|
|
516
|
+
1. Middlewares globais registrados via `app.use()`
|
|
517
|
+
2. Middlewares herdados do escopo de plugin/grupo
|
|
518
|
+
3. Middlewares específicos da rota
|
|
519
|
+
4. Handler da rota (dentro do pipeline)
|
|
520
|
+
|
|
521
|
+
> `preHandler` é executado imediatamente antes do handler, dentro do estágio final do pipeline.
|
|
522
|
+
|
|
523
|
+
**Implementação do pipeline executor:**
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
function compose(middlewares) {
|
|
527
|
+
return function (ctx) {
|
|
528
|
+
let index = -1;
|
|
529
|
+
|
|
530
|
+
function dispatch(i) {
|
|
531
|
+
if (i <= index) {
|
|
532
|
+
return Promise.reject(new Error('next() called multiple times'));
|
|
533
|
+
}
|
|
534
|
+
index = i;
|
|
535
|
+
|
|
536
|
+
const fn = middlewares[i];
|
|
537
|
+
if (!fn) return Promise.resolve();
|
|
538
|
+
|
|
539
|
+
return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return dispatch(0);
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
### 6. Plugin System
|
|
550
|
+
|
|
551
|
+
Inspirado diretamente no Fastify, com **encapsulamento de escopo**.
|
|
552
|
+
|
|
553
|
+
**Arquivos:** `src/plugins/manager.mjs`, `src/core/application.mjs` (criação de escopo)
|
|
554
|
+
|
|
555
|
+
**Conceito:**
|
|
556
|
+
|
|
557
|
+
- Cada plugin recebe uma instância "encapsulada" do app
|
|
558
|
+
- Decorators, hooks e middlewares de plugin são aplicados por escopo
|
|
559
|
+
- Plugins podem ter dependências e são registrados de forma assíncrona
|
|
560
|
+
|
|
561
|
+
**Garantias de encapsulamento no runtime atual:**
|
|
562
|
+
|
|
563
|
+
- Pai → Filho: middlewares, hooks e decorators são herdados
|
|
564
|
+
- Filho → Pai: alterações no escopo filho **não** voltam para o pai
|
|
565
|
+
- Irmão A ↔ Irmão B: não compartilham decorators/hooks/middlewares locais
|
|
566
|
+
- Prefixo: `register(plugin, { prefix })` é acumulativo em plugins aninhados
|
|
567
|
+
|
|
568
|
+
**API:**
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
// Definir um plugin
|
|
572
|
+
async function dbPlugin(app, opts) {
|
|
573
|
+
const connection = await connectDB(opts.uri);
|
|
574
|
+
|
|
575
|
+
// Decorator: adiciona propriedade ao escopo atual do plugin
|
|
576
|
+
app.decorate('db', connection);
|
|
577
|
+
|
|
578
|
+
// Hook específico do escopo
|
|
579
|
+
app.addHook('onRequest', async (ctx) => {
|
|
580
|
+
ctx.state.db = connection;
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Registrar plugin
|
|
585
|
+
app.register(dbPlugin, { uri: 'mongodb://localhost/mydb' });
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
**Encapsulamento:**
|
|
589
|
+
|
|
590
|
+
```text
|
|
591
|
+
┌─────────────────────────────────────────────┐
|
|
592
|
+
│ Root Scope (app) │
|
|
593
|
+
│ ├── global middlewares │
|
|
594
|
+
│ ├── global hooks │
|
|
595
|
+
│ │ │
|
|
596
|
+
│ │ ┌────────────────────────────────────┐ │
|
|
597
|
+
│ │ │ Plugin Scope A │ │
|
|
598
|
+
│ │ │ ├── herda middlewares do pai │ │
|
|
599
|
+
│ │ │ ├── decorators locais (scope.db) │ │
|
|
600
|
+
│ │ │ └── rotas locais (/api/v1/*) │ │
|
|
601
|
+
│ │ └────────────────────────────────────┘ │
|
|
602
|
+
│ │ │
|
|
603
|
+
│ │ ┌────────────────────────────────────┐ │
|
|
604
|
+
│ │ │ Plugin Scope B │ │
|
|
605
|
+
│ │ │ ├── herda middlewares do pai │ │
|
|
606
|
+
│ │ │ ├── NÃO acessa app.db (de A) │ │
|
|
607
|
+
│ │ │ └── rotas locais (/api/v2/*) │ │
|
|
608
|
+
│ │ └────────────────────────────────────┘ │
|
|
609
|
+
│ │ │
|
|
610
|
+
└─────────────────────────────────────────────┘
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
**Herança de escopo (resumo):**
|
|
614
|
+
|
|
615
|
+
| Recurso | Pai → Filho | Filho → Pai | Irmãos |
|
|
616
|
+
| ------------ | ----------- | ----------- | ------ |
|
|
617
|
+
| Decorators | Sim | Não | Não |
|
|
618
|
+
| Hooks | Sim | Não | Não |
|
|
619
|
+
| Middlewares | Sim | Não | Não |
|
|
620
|
+
| Prefixo rota | Sim | Não | Não |
|
|
621
|
+
|
|
622
|
+
**Exemplo com plugins irmãos (sem vazamento):**
|
|
623
|
+
|
|
624
|
+
```js
|
|
625
|
+
import { NotFoundError, zent } from 'zentjs';
|
|
626
|
+
|
|
627
|
+
const app = zent();
|
|
628
|
+
|
|
629
|
+
async function pluginA(scope) {
|
|
630
|
+
scope.decorate('token', 'A');
|
|
631
|
+
scope.get('/a', (ctx) => ctx.res.json({ token: scope.token }));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function pluginB(scope) {
|
|
635
|
+
scope.get('/b', (ctx) => {
|
|
636
|
+
if (!scope.hasDecorator('token')) {
|
|
637
|
+
throw new NotFoundError('Decorator token is not available in plugin B');
|
|
638
|
+
}
|
|
639
|
+
return ctx.res.json({ token: scope.token });
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
app.register(pluginA, { prefix: '/v1' });
|
|
644
|
+
app.register(pluginB, { prefix: '/v1' });
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
No exemplo acima, o decorator `token` criado em `pluginA` não fica visível em `pluginB`.
|
|
648
|
+
|
|
649
|
+
**Propriedades do Plugin Manager:**
|
|
650
|
+
|
|
651
|
+
```text
|
|
652
|
+
┌──────────────────────────────────────┐
|
|
653
|
+
│ PluginManager │
|
|
654
|
+
├──────────────────────────────────────┤
|
|
655
|
+
│ - _plugins: PluginEntry[] │
|
|
656
|
+
│ - _loaded: boolean │
|
|
657
|
+
├──────────────────────────────────────┤
|
|
658
|
+
│ + register(fn, opts?) │
|
|
659
|
+
│ + load(): Promise<void> │
|
|
660
|
+
│ + createScope(parent): Zent │
|
|
661
|
+
└──────────────────────────────────────┘
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
---
|
|
665
|
+
|
|
666
|
+
### 7. Lifecycle Hooks
|
|
667
|
+
|
|
668
|
+
Hooks permitem interceptar diferentes fases do ciclo de vida de uma requisição.
|
|
669
|
+
|
|
670
|
+
**Arquivo:** `src/hooks/lifecycle.mjs`
|
|
671
|
+
|
|
672
|
+
**Hooks disponíveis (em ordem de execução):**
|
|
673
|
+
|
|
674
|
+
```text
|
|
675
|
+
Requisição chega
|
|
676
|
+
│
|
|
677
|
+
▼
|
|
678
|
+
┌─────────────┐
|
|
679
|
+
│ onRequest │ → Primeira coisa executada (logging, auth check)
|
|
680
|
+
└──────┬──────┘
|
|
681
|
+
▼
|
|
682
|
+
┌─────────────┐
|
|
683
|
+
│ preParsing │ → Antes de fazer parse do body
|
|
684
|
+
└──────┬──────┘
|
|
685
|
+
▼
|
|
686
|
+
┌───────────────┐
|
|
687
|
+
│ preValidation │ → Antes de validar o input (schema)
|
|
688
|
+
└──────┬────────┘
|
|
689
|
+
▼
|
|
690
|
+
┌─────────────┐
|
|
691
|
+
│ preHandler │ → Depois de validar, antes do handler
|
|
692
|
+
└──────┬──────┘
|
|
693
|
+
▼
|
|
694
|
+
┌─────────────┐
|
|
695
|
+
│ Handler │ → Função do usuário
|
|
696
|
+
└──────┬──────┘
|
|
697
|
+
▼
|
|
698
|
+
┌─────────────┐
|
|
699
|
+
│ onSend │ → Antes de enviar a resposta (pode modificar payload)
|
|
700
|
+
└──────┬──────┘
|
|
701
|
+
▼
|
|
702
|
+
┌─────────────┐
|
|
703
|
+
│ onResponse │ → Depois que a resposta foi enviada (cleanup, metrics)
|
|
704
|
+
└──────┬──────┘
|
|
705
|
+
▼
|
|
706
|
+
Fim
|
|
707
|
+
|
|
708
|
+
┌─────────────┐
|
|
709
|
+
│ onError │ → Chamado quando qualquer erro ocorre (em qualquer fase)
|
|
710
|
+
└─────────────┘
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
**Signature dos hooks:**
|
|
714
|
+
|
|
715
|
+
```js
|
|
716
|
+
// onRequest, preParsing, preValidation, preHandler
|
|
717
|
+
app.addHook('onRequest', async (ctx) => {
|
|
718
|
+
ctx.req.startTime = Date.now();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// onSend — pode modificar o payload
|
|
722
|
+
app.addHook('onSend', async (ctx, payload) => {
|
|
723
|
+
// Retornar payload modificado
|
|
724
|
+
return payload;
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
// onResponse — chamado após envio (não pode modificar a resposta)
|
|
728
|
+
app.addHook('onResponse', async (ctx) => {
|
|
729
|
+
const duration = Date.now() - ctx.req.startTime;
|
|
730
|
+
console.log(`${ctx.req.method} ${ctx.req.path} - ${duration}ms`);
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// onError — handler de erro global
|
|
734
|
+
app.addHook('onError', async (ctx, error) => {
|
|
735
|
+
console.error(error);
|
|
736
|
+
});
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
**Ordem de execução no runtime (quando a rota existe):**
|
|
740
|
+
|
|
741
|
+
1. `onRequest` global → `onRequest` da rota
|
|
742
|
+
2. `preParsing` global → `preParsing` da rota
|
|
743
|
+
3. `preValidation` global → `preValidation` da rota
|
|
744
|
+
4. Pipeline de middlewares
|
|
745
|
+
5. `preHandler` global → `preHandler` da rota
|
|
746
|
+
6. Handler da rota
|
|
747
|
+
7. `onSend` global → `onSend` da rota (somente quando handler retorna payload e `ctx.res` ainda não foi enviado)
|
|
748
|
+
8. `onResponse` global → `onResponse` da rota
|
|
749
|
+
|
|
750
|
+
Se ocorrer erro em qualquer etapa:
|
|
751
|
+
|
|
752
|
+
1. `onError` global
|
|
753
|
+
2. `onError` da rota
|
|
754
|
+
3. `setErrorHandler()` customizado (ou handler padrão)
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
### 8. Context (ctx)
|
|
759
|
+
|
|
760
|
+
Objeto criado **por requisição** que carrega todo o estado.
|
|
761
|
+
|
|
762
|
+
**Arquivo:** `src/core/context.mjs`
|
|
763
|
+
|
|
764
|
+
```js
|
|
765
|
+
class Context {
|
|
766
|
+
constructor(req, res, app) {
|
|
767
|
+
this.req = req; // ZentRequest
|
|
768
|
+
this.res = res; // ZentResponse
|
|
769
|
+
this.app = app; // Instância da aplicação
|
|
770
|
+
this.state = {}; // Espaço livre para o usuário armazenar dados
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
**Uso no handler:**
|
|
776
|
+
|
|
777
|
+
```js
|
|
778
|
+
app.get('/dashboard', async (ctx) => {
|
|
779
|
+
// ctx.req → Request
|
|
780
|
+
// ctx.res → Response
|
|
781
|
+
// ctx.state → dados do middleware (ex: user autenticado)
|
|
782
|
+
// ctx.app → instância (acesso a decorators: ctx.app.db)
|
|
783
|
+
|
|
784
|
+
const userId = ctx.state.user.id;
|
|
785
|
+
const data = await ctx.app.db.findDashboard(userId);
|
|
786
|
+
|
|
787
|
+
return ctx.res.json(data);
|
|
788
|
+
});
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
### 9. Error Handling
|
|
794
|
+
|
|
795
|
+
Sistema de erros estruturado com classes customizadas e error handler global.
|
|
796
|
+
|
|
797
|
+
**Arquivos:** `src/errors/http-error.mjs`, `src/errors/error-handler.mjs`
|
|
798
|
+
|
|
799
|
+
**Hierarquia de erros:**
|
|
800
|
+
|
|
801
|
+
```text
|
|
802
|
+
Error
|
|
803
|
+
└── HttpError
|
|
804
|
+
├── BadRequestError (400)
|
|
805
|
+
├── UnauthorizedError (401)
|
|
806
|
+
├── ForbiddenError (403)
|
|
807
|
+
├── NotFoundError (404)
|
|
808
|
+
├── MethodNotAllowedError (405)
|
|
809
|
+
├── ConflictError (409)
|
|
810
|
+
├── UnprocessableEntityError (422)
|
|
811
|
+
├── TooManyRequestsError (429)
|
|
812
|
+
└── InternalServerError (500)
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**Uso:**
|
|
816
|
+
|
|
817
|
+
```js
|
|
818
|
+
import { NotFoundError, BadRequestError } from 'zentjs';
|
|
819
|
+
|
|
820
|
+
app.get('/users/:id', async (ctx) => {
|
|
821
|
+
if (!isValidId(ctx.req.params.id)) {
|
|
822
|
+
throw new BadRequestError('Invalid user ID');
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
const user = await findUser(ctx.req.params.id);
|
|
826
|
+
if (!user) {
|
|
827
|
+
throw new NotFoundError('User not found');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return ctx.res.json(user);
|
|
831
|
+
});
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
**Formato de resposta de erro padrão:**
|
|
835
|
+
|
|
836
|
+
```json
|
|
837
|
+
{
|
|
838
|
+
"statusCode": 404,
|
|
839
|
+
"error": "Not Found",
|
|
840
|
+
"message": "User not found"
|
|
841
|
+
}
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Comportamento padrão do ErrorHandler:**
|
|
845
|
+
|
|
846
|
+
- Se o erro já for `HttpError`, usa o `statusCode` e `toJSON()` da classe
|
|
847
|
+
- Se for erro genérico, converte para `InternalServerError` (500)
|
|
848
|
+
- Se `ctx.res.sent === true`, não envia resposta duplicada
|
|
849
|
+
- Se `setErrorHandler()` falhar, faz fallback para o handler padrão
|
|
850
|
+
|
|
851
|
+
**404 customizado com `setNotFoundHandler()`:**
|
|
852
|
+
|
|
853
|
+
```js
|
|
854
|
+
app.setNotFoundHandler(async (ctx) => {
|
|
855
|
+
return ctx.res.status(404).json({
|
|
856
|
+
statusCode: 404,
|
|
857
|
+
error: 'Not Found',
|
|
858
|
+
message: `Route ${ctx.req.method} ${ctx.req.path} not found`,
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
Se o not-found handler não enviar resposta, o framework envia automaticamente o payload padrão de `NotFoundError`.
|
|
864
|
+
|
|
865
|
+
**Falha comum de parsing (JSON inválido):**
|
|
866
|
+
|
|
867
|
+
```js
|
|
868
|
+
import { bodyParser, zent } from 'zentjs';
|
|
869
|
+
|
|
870
|
+
const app = zent();
|
|
871
|
+
app.use(bodyParser());
|
|
872
|
+
|
|
873
|
+
app.post('/echo', (ctx) => ctx.res.json(ctx.req.body));
|
|
874
|
+
// body JSON inválido -> BadRequestError(400) com message "Invalid JSON body"
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
**Error handler customizado:**
|
|
878
|
+
|
|
879
|
+
```js
|
|
880
|
+
app.setErrorHandler((error, ctx) => {
|
|
881
|
+
// Lógica customizada
|
|
882
|
+
return ctx.res.status(error.statusCode || 500).json({
|
|
883
|
+
success: false,
|
|
884
|
+
error: error.message,
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Fluxo de uma Requisição
|
|
892
|
+
|
|
893
|
+
Diagrama completo do ciclo de vida de uma requisição HTTP no ZentJS:
|
|
894
|
+
|
|
895
|
+
```text
|
|
896
|
+
Cliente HTTP
|
|
897
|
+
│
|
|
898
|
+
▼
|
|
899
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
900
|
+
│ node:http Server │
|
|
901
|
+
│ (req, res) callback │
|
|
902
|
+
└───────────────────────┬──────────────────────────────────────┘
|
|
903
|
+
│
|
|
904
|
+
▼
|
|
905
|
+
┌─────────────────┐
|
|
906
|
+
│ Criar Context │ ZentRequest + ZentResponse
|
|
907
|
+
│ (ctx) │ são instanciados
|
|
908
|
+
└────────┬────────┘
|
|
909
|
+
│
|
|
910
|
+
▼
|
|
911
|
+
┌──────────────────┐
|
|
912
|
+
│ onRequest hooks │ Logging, rate limiting, etc.
|
|
913
|
+
└────────┬─────────┘
|
|
914
|
+
│
|
|
915
|
+
▼
|
|
916
|
+
┌──────────────────┐
|
|
917
|
+
│ Router.find() │ Radix Tree lookup
|
|
918
|
+
│ (method + path) │ → handler + params + route hooks
|
|
919
|
+
└────────┬─────────┘
|
|
920
|
+
│
|
|
921
|
+
┌────────┴──────────┐
|
|
922
|
+
│ Rota encontrada? │
|
|
923
|
+
└────┬────────┬─────┘
|
|
924
|
+
│ Não │ Sim
|
|
925
|
+
▼ ▼
|
|
926
|
+
┌───────────┐ ┌─────────────────┐
|
|
927
|
+
│ 404 Error │ │ preParsing │ body-parser, upload
|
|
928
|
+
└───────────┘ └───────┬─────────┘
|
|
929
|
+
│
|
|
930
|
+
▼
|
|
931
|
+
┌─────────────────┐
|
|
932
|
+
│ preValidation │ schema validation
|
|
933
|
+
└────────┬────────┘
|
|
934
|
+
│
|
|
935
|
+
▼
|
|
936
|
+
┌──────────────────────┐
|
|
937
|
+
│ Middleware Pipeline │ Global + Route middlewares
|
|
938
|
+
│ (onion model) │
|
|
939
|
+
└──────────┬───────────┘
|
|
940
|
+
│
|
|
941
|
+
▼
|
|
942
|
+
┌─────────────────┐
|
|
943
|
+
│ preHandler │ Última chance antes do handler
|
|
944
|
+
└────────┬────────┘
|
|
945
|
+
│
|
|
946
|
+
▼
|
|
947
|
+
┌─────────────────┐
|
|
948
|
+
│ Route Handler │ Função do usuário
|
|
949
|
+
└────────┬────────┘
|
|
950
|
+
│
|
|
951
|
+
▼
|
|
952
|
+
┌─────────────────┐
|
|
953
|
+
│ onSend │ Serialização, compressão
|
|
954
|
+
└────────┬────────┘
|
|
955
|
+
│
|
|
956
|
+
▼
|
|
957
|
+
┌─────────────────┐
|
|
958
|
+
│ res.end() │ Resposta enviada ao cliente
|
|
959
|
+
└────────┬────────┘
|
|
960
|
+
│
|
|
961
|
+
▼
|
|
962
|
+
┌─────────────────┐
|
|
963
|
+
│ onResponse │ Métricas, cleanup
|
|
964
|
+
└─────────────────┘
|
|
965
|
+
|
|
966
|
+
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
967
|
+
Em caso de erro em qualquer fase:
|
|
968
|
+
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
|
969
|
+
┌─────────────────┐
|
|
970
|
+
│ onError hook │
|
|
971
|
+
│ Error Handler │ Formata e envia erro
|
|
972
|
+
└─────────────────┘
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
---
|
|
976
|
+
|
|
977
|
+
## API Pública
|
|
978
|
+
|
|
979
|
+
### Criação da instância
|
|
980
|
+
|
|
981
|
+
```js
|
|
982
|
+
import { zent } from 'zentjs';
|
|
983
|
+
|
|
984
|
+
const app = zent(options?);
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
**Opções:**
|
|
988
|
+
|
|
989
|
+
| Opção | Tipo | Default | Descrição |
|
|
990
|
+
| --------------------- | --------- | ------- | --------------------------------- |
|
|
991
|
+
| `ignoreTrailingSlash` | `boolean` | `true` | `/foo` e `/foo/` são equivalentes |
|
|
992
|
+
| `caseSensitive` | `boolean` | `false` | Paths sensíveis a maiúsculas |
|
|
993
|
+
|
|
994
|
+
> As opções acima são as efetivamente consumidas pela aplicação no runtime atual.
|
|
995
|
+
|
|
996
|
+
### Métodos de roteamento
|
|
997
|
+
|
|
998
|
+
```js
|
|
999
|
+
app.get(path, handler, options?);
|
|
1000
|
+
app.post(path, handler, options?);
|
|
1001
|
+
app.put(path, handler, options?);
|
|
1002
|
+
app.patch(path, handler, options?);
|
|
1003
|
+
app.delete(path, handler, options?);
|
|
1004
|
+
app.head(path, handler, options?);
|
|
1005
|
+
app.options(path, handler, options?);
|
|
1006
|
+
app.all(path, handler, options?); // todos os métodos HTTP
|
|
1007
|
+
app.route(definition); // definição completa da rota
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**`options` por rota:**
|
|
1011
|
+
|
|
1012
|
+
```js
|
|
1013
|
+
{
|
|
1014
|
+
middlewares: [authMiddleware],
|
|
1015
|
+
hooks: {
|
|
1016
|
+
onRequest: [fn],
|
|
1017
|
+
preParsing: [fn],
|
|
1018
|
+
preValidation: [fn],
|
|
1019
|
+
preHandler: [fn],
|
|
1020
|
+
onSend: [fn],
|
|
1021
|
+
onResponse: [fn],
|
|
1022
|
+
onError: [fn],
|
|
1023
|
+
},
|
|
1024
|
+
}
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
**`app.route(definition)`**
|
|
1028
|
+
|
|
1029
|
+
```js
|
|
1030
|
+
app.route({
|
|
1031
|
+
method: 'POST',
|
|
1032
|
+
path: '/users',
|
|
1033
|
+
handler: createUser,
|
|
1034
|
+
middlewares: [authMiddleware],
|
|
1035
|
+
hooks: { preValidation: [validateBody] },
|
|
1036
|
+
});
|
|
1037
|
+
```
|
|
1038
|
+
|
|
1039
|
+
### Middleware
|
|
1040
|
+
|
|
1041
|
+
```js
|
|
1042
|
+
app.use(middleware); // Global
|
|
1043
|
+
app.use('/api', middleware); // Com prefixo
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
Assinaturas válidas:
|
|
1047
|
+
|
|
1048
|
+
- `use(middleware)`
|
|
1049
|
+
- `use(prefix, middleware)`
|
|
1050
|
+
|
|
1051
|
+
### Route Groups
|
|
1052
|
+
|
|
1053
|
+
Agrupa rotas sob um prefixo compartilhado, com middlewares e hooks herdados:
|
|
1054
|
+
|
|
1055
|
+
```js
|
|
1056
|
+
// Grupo simples
|
|
1057
|
+
app.group('/api/v1', (group) => {
|
|
1058
|
+
group.get('/users', listUsers); // GET /api/v1/users
|
|
1059
|
+
group.post('/users', createUser); // POST /api/v1/users
|
|
1060
|
+
group.get('/users/:id', getUser); // GET /api/v1/users/:id
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// Grupo com middlewares compartilhados
|
|
1064
|
+
app.group('/admin', { middlewares: [authMiddleware] }, (group) => {
|
|
1065
|
+
group.get('/dashboard', dashboard); // GET /admin/dashboard (com auth)
|
|
1066
|
+
group.delete('/users/:id', deleteUser);
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
// Sub-grupos aninhados (middlewares acumulam: pai → filho → rota)
|
|
1070
|
+
app.group('/api', { middlewares: [cors] }, (api) => {
|
|
1071
|
+
api.group('/v1', { middlewares: [rateLimit] }, (v1) => {
|
|
1072
|
+
v1.get('/products', listProducts); // middlewares: [cors, rateLimit]
|
|
1073
|
+
});
|
|
1074
|
+
api.group('/v2', (v2) => {
|
|
1075
|
+
v2.get('/products', listProductsV2); // middlewares: [cors]
|
|
1076
|
+
});
|
|
1077
|
+
});
|
|
1078
|
+
```
|
|
1079
|
+
|
|
1080
|
+
**Características:**
|
|
1081
|
+
|
|
1082
|
+
- Prefixo aplicado automaticamente a todas as rotas do grupo
|
|
1083
|
+
- Middlewares do grupo executam **antes** dos middlewares da rota
|
|
1084
|
+
- Hooks são mesclados (grupo → rota)
|
|
1085
|
+
- Sub-grupos herdam middlewares/hooks dos grupos pai
|
|
1086
|
+
- Mesma API de conveniência (`get`, `post`, `all`, `route`, etc.)
|
|
1087
|
+
|
|
1088
|
+
### Plugins
|
|
1089
|
+
|
|
1090
|
+
```js
|
|
1091
|
+
app.register(plugin, options?); // ex: { prefix: '/api' }
|
|
1092
|
+
app.decorate(name, value);
|
|
1093
|
+
app.hasDecorator(name);
|
|
1094
|
+
```
|
|
1095
|
+
|
|
1096
|
+
Contrato de plugin:
|
|
1097
|
+
|
|
1098
|
+
```js
|
|
1099
|
+
async function myPlugin(scope, opts) {
|
|
1100
|
+
// scope possui API compatível com app
|
|
1101
|
+
// e respeita encapsulamento por escopo
|
|
1102
|
+
}
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
Métrica mínima por hooks (built-in):
|
|
1106
|
+
|
|
1107
|
+
```js
|
|
1108
|
+
import { requestMetrics } from 'zentjs';
|
|
1109
|
+
|
|
1110
|
+
const metrics = requestMetrics({
|
|
1111
|
+
onRecord: (record) => {
|
|
1112
|
+
// { method, path, statusCode, durationMs }
|
|
1113
|
+
console.log(record);
|
|
1114
|
+
},
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
app.addHook('onRequest', metrics.onRequest);
|
|
1118
|
+
app.addHook('onResponse', metrics.onResponse);
|
|
1119
|
+
```
|
|
1120
|
+
|
|
1121
|
+
### Lifecycle
|
|
1122
|
+
|
|
1123
|
+
```js
|
|
1124
|
+
app.addHook(hookName, hookFunction);
|
|
1125
|
+
app.setErrorHandler(handler);
|
|
1126
|
+
app.setNotFoundHandler(handler);
|
|
1127
|
+
```
|
|
1128
|
+
|
|
1129
|
+
`hookName` suportados no lifecycle global:
|
|
1130
|
+
|
|
1131
|
+
- `onRequest`
|
|
1132
|
+
- `preParsing`
|
|
1133
|
+
- `preValidation`
|
|
1134
|
+
- `preHandler`
|
|
1135
|
+
- `onSend`
|
|
1136
|
+
- `onResponse`
|
|
1137
|
+
- `onError`
|
|
1138
|
+
|
|
1139
|
+
### Servidor
|
|
1140
|
+
|
|
1141
|
+
```js
|
|
1142
|
+
await app.listen({ port, host }, callback?); // default: 3000 / 0.0.0.0
|
|
1143
|
+
await app.close();
|
|
1144
|
+
const response = await app.inject({ method, url, headers?, body? });
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
Resposta de `inject()`:
|
|
1148
|
+
|
|
1149
|
+
```js
|
|
1150
|
+
{
|
|
1151
|
+
(statusCode, headers, body, json()); // helper para JSON.parse(body)
|
|
1152
|
+
}
|
|
1153
|
+
```
|
|
1154
|
+
|
|
1155
|
+
---
|
|
1156
|
+
|
|
1157
|
+
## Exemplos de Uso
|
|
1158
|
+
|
|
1159
|
+
### Exemplos executáveis (Fase 5)
|
|
1160
|
+
|
|
1161
|
+
Os exemplos abaixo já estão prontos na pasta `examples/`:
|
|
1162
|
+
|
|
1163
|
+
```bash
|
|
1164
|
+
node examples/hello-world.mjs
|
|
1165
|
+
node examples/rest-api.mjs
|
|
1166
|
+
node examples/with-plugins.mjs
|
|
1167
|
+
```
|
|
1168
|
+
|
|
1169
|
+
Cada exemplo sobe o servidor em `127.0.0.1:3000`.
|
|
1170
|
+
|
|
1171
|
+
Benchmark básico (Fase 10):
|
|
1172
|
+
|
|
1173
|
+
```bash
|
|
1174
|
+
npm run bench
|
|
1175
|
+
npm run bench:save-baseline
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### Guias Práticos (Etapa 5)
|
|
1179
|
+
|
|
1180
|
+
- CRUD básico: seção **Guia: Primeira API (CRUD básico)**
|
|
1181
|
+
- Autenticação por escopo: seção **Guia: Autenticação por plugin**
|
|
1182
|
+
- Métricas por requisição: seção **Guia: Métricas com requestMetrics**
|
|
1183
|
+
- Testes sem rede: seção **Guia: Testes com inject + Vitest**
|
|
1184
|
+
|
|
1185
|
+
### Hello World
|
|
1186
|
+
|
|
1187
|
+
```js
|
|
1188
|
+
import { zent } from 'zentjs';
|
|
1189
|
+
|
|
1190
|
+
const app = zent();
|
|
1191
|
+
|
|
1192
|
+
app.get('/', (ctx) => {
|
|
1193
|
+
return ctx.res.json({ hello: 'world' });
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
app.listen({ port: 3000 });
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
### Guia: Primeira API (CRUD básico)
|
|
1200
|
+
|
|
1201
|
+
```js
|
|
1202
|
+
import { zent, bodyParser, NotFoundError } from 'zentjs';
|
|
1203
|
+
|
|
1204
|
+
const app = zent();
|
|
1205
|
+
|
|
1206
|
+
// Middleware global para parsear body
|
|
1207
|
+
app.use(bodyParser());
|
|
1208
|
+
|
|
1209
|
+
// In-memory store
|
|
1210
|
+
const users = new Map();
|
|
1211
|
+
let nextId = 1;
|
|
1212
|
+
|
|
1213
|
+
app.get('/users', (ctx) => {
|
|
1214
|
+
return ctx.res.json([...users.values()]);
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
app.get('/users/:id', (ctx) => {
|
|
1218
|
+
const user = users.get(Number(ctx.req.params.id));
|
|
1219
|
+
if (!user) throw new NotFoundError('User not found');
|
|
1220
|
+
return ctx.res.json(user);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
app.post('/users', (ctx) => {
|
|
1224
|
+
const { name, email } = ctx.req.body;
|
|
1225
|
+
const user = { id: nextId++, name, email };
|
|
1226
|
+
users.set(user.id, user);
|
|
1227
|
+
return ctx.res.status(201).json(user);
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
app.put('/users/:id', (ctx) => {
|
|
1231
|
+
const id = Number(ctx.req.params.id);
|
|
1232
|
+
if (!users.has(id)) throw new NotFoundError('User not found');
|
|
1233
|
+
|
|
1234
|
+
const { name, email } = ctx.req.body;
|
|
1235
|
+
const user = { id, name, email };
|
|
1236
|
+
users.set(id, user);
|
|
1237
|
+
return ctx.res.json(user);
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
app.delete('/users/:id', (ctx) => {
|
|
1241
|
+
const id = Number(ctx.req.params.id);
|
|
1242
|
+
if (!users.has(id)) throw new NotFoundError('User not found');
|
|
1243
|
+
|
|
1244
|
+
users.delete(id);
|
|
1245
|
+
return ctx.res.empty();
|
|
1246
|
+
});
|
|
1247
|
+
|
|
1248
|
+
app.listen({ port: 3000 });
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
### Guia: Autenticação por plugin
|
|
1252
|
+
|
|
1253
|
+
```js
|
|
1254
|
+
import { UnauthorizedError, zent } from 'zentjs';
|
|
1255
|
+
|
|
1256
|
+
// Plugin de autenticação
|
|
1257
|
+
async function authPlugin(app, opts) {
|
|
1258
|
+
app.decorate('authenticate', async (ctx) => {
|
|
1259
|
+
const token = ctx.req.get('authorization');
|
|
1260
|
+
if (!token) throw new UnauthorizedError('Missing token');
|
|
1261
|
+
ctx.state.user = verifyToken(token);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
// Aplica hook em todas as rotas dentro deste escopo
|
|
1265
|
+
app.addHook('preHandler', async (ctx) => {
|
|
1266
|
+
await app.authenticate(ctx);
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Plugin de rotas protegidas
|
|
1271
|
+
async function protectedRoutes(app) {
|
|
1272
|
+
// Registra o plugin de auth neste escopo
|
|
1273
|
+
app.register(authPlugin);
|
|
1274
|
+
|
|
1275
|
+
app.get('/profile', (ctx) => {
|
|
1276
|
+
return ctx.res.json(ctx.state.user);
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// Plugin de rotas públicas
|
|
1281
|
+
async function publicRoutes(app) {
|
|
1282
|
+
app.get('/health', (ctx) => {
|
|
1283
|
+
return ctx.res.json({ status: 'ok' });
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const app = zent();
|
|
1288
|
+
|
|
1289
|
+
// Rotas públicas (sem auth)
|
|
1290
|
+
app.register(publicRoutes, { prefix: '/api' });
|
|
1291
|
+
|
|
1292
|
+
// Rotas protegidas (com auth)
|
|
1293
|
+
app.register(protectedRoutes, { prefix: '/api' });
|
|
1294
|
+
|
|
1295
|
+
app.listen({ port: 3000 });
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### Guia: Métricas com requestMetrics
|
|
1299
|
+
|
|
1300
|
+
```js
|
|
1301
|
+
import { requestMetrics, zent } from 'zentjs';
|
|
1302
|
+
|
|
1303
|
+
const app = zent();
|
|
1304
|
+
|
|
1305
|
+
const metrics = requestMetrics({
|
|
1306
|
+
onRecord: (record) => {
|
|
1307
|
+
// { method, path, statusCode, durationMs }
|
|
1308
|
+
console.log(record);
|
|
1309
|
+
},
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
app.addHook('onRequest', metrics.onRequest);
|
|
1313
|
+
app.addHook('onResponse', metrics.onResponse);
|
|
1314
|
+
|
|
1315
|
+
app.get('/health', (ctx) => {
|
|
1316
|
+
return ctx.res.json({ status: 'ok' });
|
|
1317
|
+
});
|
|
1318
|
+
|
|
1319
|
+
app.listen({ port: 3000 });
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
### Guia: Testes com inject + Vitest
|
|
1323
|
+
|
|
1324
|
+
```js
|
|
1325
|
+
import { describe, it, expect } from 'vitest';
|
|
1326
|
+
import { zent } from 'zentjs';
|
|
1327
|
+
|
|
1328
|
+
describe('API', () => {
|
|
1329
|
+
it('should return hello world', async () => {
|
|
1330
|
+
const app = zent();
|
|
1331
|
+
|
|
1332
|
+
app.get('/', (ctx) => {
|
|
1333
|
+
return ctx.res.json({ hello: 'world' });
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
const response = await app.inject({
|
|
1337
|
+
method: 'GET',
|
|
1338
|
+
url: '/',
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
expect(response.statusCode).toBe(200);
|
|
1342
|
+
expect(response.json()).toEqual({ hello: 'world' });
|
|
1343
|
+
});
|
|
1344
|
+
});
|
|
1345
|
+
```
|
|
1346
|
+
|
|
1347
|
+
---
|
|
1348
|
+
|
|
1349
|
+
## Roadmap de Implementação
|
|
1350
|
+
|
|
1351
|
+
A implementação segue uma ordem lógica de dependências:
|
|
1352
|
+
|
|
1353
|
+
### Fase 1 — Fundação (Core)
|
|
1354
|
+
|
|
1355
|
+
| # | Módulo | Prioridade | Dependência | Descrição |
|
|
1356
|
+
| --- | -------------- | ---------- | ----------- | ---------------------------------- |
|
|
1357
|
+
| 1 | `HttpError` | Alta | Nenhuma | Classes de erro HTTP |
|
|
1358
|
+
| 2 | `ZentRequest` | Alta | Nenhuma | Wrapper do IncomingMessage |
|
|
1359
|
+
| 3 | `ZentResponse` | Alta | Nenhuma | Wrapper do ServerResponse |
|
|
1360
|
+
| 4 | `Context` | Alta | 2, 3 | Objeto de contexto (req + res) |
|
|
1361
|
+
| 5 | `RadixTree` | Alta | Nenhuma | Estrutura de dados para roteamento |
|
|
1362
|
+
| 6 | `Router` | Alta | 5 | API pública do router |
|
|
1363
|
+
|
|
1364
|
+
### Fase 2 — Pipeline
|
|
1365
|
+
|
|
1366
|
+
| # | Módulo | Prioridade | Dependência | Descrição |
|
|
1367
|
+
| --- | -------------- | ---------- | ----------- | --------------------------------- |
|
|
1368
|
+
| 7 | `Pipeline` | Alta | 4 | Executor de middlewares (compose) |
|
|
1369
|
+
| 8 | `Lifecycle` | Alta | 7 | Gerenciador de hooks |
|
|
1370
|
+
| 9 | `ErrorHandler` | Alta | 1, 4 | Handler global de erros |
|
|
1371
|
+
|
|
1372
|
+
### Fase 3 — Aplicação
|
|
1373
|
+
|
|
1374
|
+
| # | Módulo | Prioridade | Dependência | Descrição |
|
|
1375
|
+
| --- | ------------- | ---------- | ------------- | ------------------------------------------ |
|
|
1376
|
+
| 10 | `HttpServer` | Alta | 4, 6, 7, 8, 9 | Servidor HTTP + request dispatch |
|
|
1377
|
+
| 11 | `Application` | Alta | 10 | Classe principal Zent |
|
|
1378
|
+
| 12 | `inject()` | Média | 10, 11 | Light-weight request injection para testes |
|
|
1379
|
+
|
|
1380
|
+
### Fase 4 — Plugins e Extras
|
|
1381
|
+
|
|
1382
|
+
| # | Módulo | Prioridade | Dependência | Descrição |
|
|
1383
|
+
| --- | --------------- | ---------- | ----------- | ----------------------------------------------- |
|
|
1384
|
+
| 13 | `PluginManager` | Média | 11 | Sistema de registro e encapsulamento de plugins |
|
|
1385
|
+
| 14 | `bodyParser` | Média | Nenhuma | Middleware built-in para parsing de body |
|
|
1386
|
+
| 15 | `cors` | Baixa | Nenhuma | Middleware built-in para CORS |
|
|
1387
|
+
|
|
1388
|
+
### Fase 5 — Polish
|
|
1389
|
+
|
|
1390
|
+
| # | Módulo | Prioridade | Descrição |
|
|
1391
|
+
| --- | -------------------- | ---------- | ----------------------------------- |
|
|
1392
|
+
| 16 | Testes de integração | Alta | Testes end-to-end HTTP reais |
|
|
1393
|
+
| 17 | JSDoc + tipos | Média | Documentação inline e type hints |
|
|
1394
|
+
| 18 | Exemplos | Baixa | Exemplos executáveis em `examples/` |
|
|
1395
|
+
|
|
1396
|
+
Status atual: fases 1–5 implementadas.
|
|
1397
|
+
|
|
1398
|
+
Métricas atuais (03/03/2026):
|
|
1399
|
+
|
|
1400
|
+
- Testes: `322/322` passando (`14` arquivos de teste)
|
|
1401
|
+
- Cobertura geral: `99.62%` statements · `96.79%` branches · `100%` functions · `99.61%` lines
|
|
1402
|
+
|
|
1403
|
+
### Novo ciclo (pós-fases iniciais)
|
|
1404
|
+
|
|
1405
|
+
Com as fases 1–5 concluídas, o próximo ciclo passa a ser guiado por **entregas pequenas e testáveis**, sempre com:
|
|
1406
|
+
|
|
1407
|
+
1. Escopo fechado por fase
|
|
1408
|
+
2. Testes unitários obrigatórios
|
|
1409
|
+
3. Testes de integração quando houver impacto no fluxo HTTP real, plugins ou encapsulamento
|
|
1410
|
+
|
|
1411
|
+
### Fase 6 — Paridade API x Runtime
|
|
1412
|
+
|
|
1413
|
+
Objetivo: alinhar comportamento real com a API pública/documentação.
|
|
1414
|
+
|
|
1415
|
+
| Item | Escopo |
|
|
1416
|
+
| ---- | -------------------------------------------------------------------------------- |
|
|
1417
|
+
| 6.1 | Executar `onSend` no dispatch da requisição (incluindo transformação de payload) |
|
|
1418
|
+
| 6.2 | Suporte completo a `app.use('/prefix', middleware)` |
|
|
1419
|
+
| 6.3 | Implementar `setNotFoundHandler()` na aplicação |
|
|
1420
|
+
| 6.4 | Garantir hooks de rota além de `preHandler` conforme contrato público |
|
|
1421
|
+
|
|
1422
|
+
**Testes da fase 6:**
|
|
1423
|
+
|
|
1424
|
+
- Unitários para `application`, `lifecycle`, `router`
|
|
1425
|
+
- Integração para validar `onSend`, middleware com prefixo e 404 customizado
|
|
1426
|
+
|
|
1427
|
+
### Fase 7 — Encapsulamento real de plugins
|
|
1428
|
+
|
|
1429
|
+
Objetivo: garantir isolamento entre escopos pai/filho/irmãos.
|
|
1430
|
+
|
|
1431
|
+
| Item | Escopo |
|
|
1432
|
+
| ---- | ------------------------------------------------------------- |
|
|
1433
|
+
| 7.1 | Isolar decorators por escopo de plugin |
|
|
1434
|
+
| 7.2 | Isolar hooks e middlewares com herança controlada pai → filho |
|
|
1435
|
+
| 7.3 | Validar que plugins irmãos não compartilham estado interno |
|
|
1436
|
+
| 7.4 | Fortalecer contratos de registro/carregamento em cascata |
|
|
1437
|
+
|
|
1438
|
+
**Testes da fase 7:**
|
|
1439
|
+
|
|
1440
|
+
- Unitários para `plugin-manager` e criação de escopo
|
|
1441
|
+
- Integração com plugins aninhados e cenários de não-vazamento
|
|
1442
|
+
|
|
1443
|
+
### Fase 8 — Robustez de HTTP/erros
|
|
1444
|
+
|
|
1445
|
+
Objetivo: endurecer comportamento em cenários de borda.
|
|
1446
|
+
|
|
1447
|
+
| Item | Escopo |
|
|
1448
|
+
| ---- | ------------------------------------------------------------------ |
|
|
1449
|
+
| 8.1 | Revisar fluxo de erro para evitar respostas duplicadas |
|
|
1450
|
+
| 8.2 | Consolidar resposta de parse inválido de body (ex.: JSON inválido) |
|
|
1451
|
+
| 8.3 | Melhorar consistência entre `inject()` e servidor real |
|
|
1452
|
+
| 8.4 | Cobrir cenários limite de headers/body/status |
|
|
1453
|
+
|
|
1454
|
+
**Testes da fase 8:**
|
|
1455
|
+
|
|
1456
|
+
- Unitários focados em `error-handler`, `body-parser`, `response`
|
|
1457
|
+
- Integração para falhas reais de parsing e serialização
|
|
1458
|
+
|
|
1459
|
+
### Fase 9 — Qualidade de documentação e DX
|
|
1460
|
+
|
|
1461
|
+
Objetivo: manter documentação e uso prático sempre sincronizados.
|
|
1462
|
+
|
|
1463
|
+
| Item | Escopo |
|
|
1464
|
+
| ---- | --------------------------------------------------------------- |
|
|
1465
|
+
| 9.1 | Corrigir lint de markdown (fenced blocks com linguagem) |
|
|
1466
|
+
| 9.2 | Revisar README para refletir somente comportamento implementado |
|
|
1467
|
+
| 9.3 | Padronizar exemplos para cobrir APIs críticas do ciclo 6–8 |
|
|
1468
|
+
|
|
1469
|
+
**Testes/validações da fase 9:**
|
|
1470
|
+
|
|
1471
|
+
- `npm run lint`
|
|
1472
|
+
- Execução dos exemplos e smoke tests de rotas principais
|
|
1473
|
+
|
|
1474
|
+
### Fase 10 — Performance e observabilidade mínima
|
|
1475
|
+
|
|
1476
|
+
Objetivo: preparar baseline para evolução com segurança.
|
|
1477
|
+
|
|
1478
|
+
| Item | Escopo |
|
|
1479
|
+
| ---- | ---------------------------------------------------------- |
|
|
1480
|
+
| 10.1 | Benchmark básico de roteamento e pipeline |
|
|
1481
|
+
| 10.2 | Métricas mínimas por requisição (tempo e status) via hooks |
|
|
1482
|
+
| 10.3 | Cenários de carga leve para regressão de performance |
|
|
1483
|
+
|
|
1484
|
+
**Testes/validações da fase 10:**
|
|
1485
|
+
|
|
1486
|
+
- Benchmarks reproduzíveis versionados no repositório
|
|
1487
|
+
- Regressão comparativa entre versões do core
|
|
1488
|
+
|
|
1489
|
+
---
|
|
1490
|
+
|
|
1491
|
+
## Decisões Técnicas (ADRs)
|
|
1492
|
+
|
|
1493
|
+
### ADR-001: ESM Only
|
|
1494
|
+
|
|
1495
|
+
**Contexto:** Node.js suporta CommonJS e ESM.
|
|
1496
|
+
**Decisão:** Usar exclusivamente ESM (`.mjs` ou `"type": "module"`).
|
|
1497
|
+
**Motivo:** ESM é o padrão do futuro, permite top-level await, tree-shaking nativo, e importações estáticas para analysis.
|
|
1498
|
+
|
|
1499
|
+
### ADR-002: Zero Dependências de Runtime
|
|
1500
|
+
|
|
1501
|
+
**Contexto:** Frameworks como Express dependem de dezenas de pacotes.
|
|
1502
|
+
**Decisão:** Nenhuma dependência no `dependencies` do package.json.
|
|
1503
|
+
**Motivo:** Reduz supply chain risk, tamanho do `node_modules`, e garante total controle sobre o código.
|
|
1504
|
+
|
|
1505
|
+
### ADR-003: Radix Tree para Roteamento
|
|
1506
|
+
|
|
1507
|
+
**Contexto:** Express usa array linear O(n), o que não escala.
|
|
1508
|
+
**Decisão:** Implementar Radix Tree customizada.
|
|
1509
|
+
**Motivo:** Lookup em O(k) onde k = comprimento do path. Performance independente do número de rotas.
|
|
1510
|
+
|
|
1511
|
+
### ADR-004: Context Object (ctx)
|
|
1512
|
+
|
|
1513
|
+
**Contexto:** Express passa `(req, res, next)`, Fastify passa `(request, reply)`.
|
|
1514
|
+
**Decisão:** Usar um único objeto `ctx` que contém `req`, `res`, `state` e `app`.
|
|
1515
|
+
**Motivo:** Simplifica a signature dos handlers, facilita extensão via `state`, e permite tipagem mais clara.
|
|
1516
|
+
|
|
1517
|
+
### ADR-005: Async-first
|
|
1518
|
+
|
|
1519
|
+
**Contexto:** Express não trata promises automaticamente.
|
|
1520
|
+
**Decisão:** Todos os handlers e middlewares são tratados como async por padrão.
|
|
1521
|
+
**Motivo:** Elimina a necessidade de `try/catch` manual e `next(err)`. Erros em async handlers são capturados automaticamente.
|
|
1522
|
+
|
|
1523
|
+
### ADR-006: Plugin Encapsulation
|
|
1524
|
+
|
|
1525
|
+
**Contexto:** Em Express, todos os middlewares são globais.
|
|
1526
|
+
**Decisão:** Plugins criam escopos encapsulados (inspirado no Fastify).
|
|
1527
|
+
**Motivo:** Evita efeitos colaterais entre módulos, facilita composição de aplicações grandes.
|
|
1528
|
+
|
|
1529
|
+
### ADR-007: Lazy Body Parsing
|
|
1530
|
+
|
|
1531
|
+
**Contexto:** Fastify parseia body apenas quando necessário.
|
|
1532
|
+
**Decisão:** Body não é parseado automaticamente — exige middleware explícito.
|
|
1533
|
+
**Motivo:** Zero overhead para rotas que não precisam de body (GET, DELETE, health checks).
|
|
1534
|
+
|
|
1535
|
+
---
|
|
1536
|
+
|
|
1537
|
+
## Contribuição
|
|
1538
|
+
|
|
1539
|
+
Este projeto valida commits com **Conventional Commits** via `commitlint`.
|
|
1540
|
+
|
|
1541
|
+
Formato esperado:
|
|
1542
|
+
|
|
1543
|
+
```text
|
|
1544
|
+
tipo(escopo-opcional): descrição
|
|
1545
|
+
```
|
|
1546
|
+
|
|
1547
|
+
Tipos aceitos pelo projeto:
|
|
1548
|
+
|
|
1549
|
+
- `build`, `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `style`, `test`
|
|
1550
|
+
|
|
1551
|
+
Exemplos válidos:
|
|
1552
|
+
|
|
1553
|
+
- `feat(router): adiciona suporte a wildcard`
|
|
1554
|
+
- `fix(cors): corrige headers do preflight`
|
|
1555
|
+
- `docs: atualiza README com badges`
|
|
1556
|
+
- `test: cobre fluxo de plugins`
|
|
1557
|
+
- `chore: ajusta workflow de release`
|
|
1558
|
+
|
|
1559
|
+
Para mudanças incompatíveis, use `!` após o tipo/escopo:
|
|
1560
|
+
|
|
1561
|
+
- `feat!: remove API legada de register`
|
|
1562
|
+
|
|
1563
|
+
ou informe no corpo do commit:
|
|
1564
|
+
|
|
1565
|
+
```text
|
|
1566
|
+
BREAKING CHANGE: descrição da mudança incompatível
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1571
|
+
## Referências
|
|
1572
|
+
|
|
1573
|
+
- [Node.js HTTP Module](https://nodejs.org/api/http.html)
|
|
1574
|
+
- [Express.js Source Code](https://github.com/expressjs/express)
|
|
1575
|
+
- [Fastify Architecture](https://fastify.dev/docs/latest/Reference/Architecture/)
|
|
1576
|
+
- [Radix Tree (Wikipedia)](https://en.wikipedia.org/wiki/Radix_tree)
|
|
1577
|
+
- [find-my-way (Fastify Router)](https://github.com/delvedor/find-my-way)
|
|
1578
|
+
- [Koa Compose (Middleware)](https://github.com/koajs/compose)
|
|
1579
|
+
|
|
1580
|
+
---
|
|
1581
|
+
|
|
1582
|
+
## Licença
|
|
1583
|
+
|
|
1584
|
+
[BSD-3-Clause](LICENSE)
|