azurajs 1.0.4 → 2.1.0

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/README.md CHANGED
@@ -31,12 +31,43 @@ or with Bun:
31
31
  bun add azurajs
32
32
  ```
33
33
 
34
+ ## Modular Imports
35
+
36
+ AzuraJS supports modular imports for tree-shaking and better organization:
37
+
38
+ ```typescript
39
+ // Main package
40
+ import { AzuraClient, applyDecorators } from "azurajs";
41
+
42
+ // Decorators
43
+ import { Controller, Get, Post, Body, Param } from "azurajs/decorators";
44
+
45
+ // Middleware
46
+ import { createLoggingMiddleware } from "azurajs/middleware";
47
+
48
+ // Plugins
49
+ import { cors } from "azurajs/cors";
50
+ import { rateLimit } from "azurajs/rate-limit";
51
+
52
+ // Utilities
53
+ import { logger } from "azurajs/logger";
54
+ import { HttpError } from "azurajs/http-error";
55
+ import { validateDto } from "azurajs/validators";
56
+ import { parseCookiesHeader } from "azurajs/cookies";
57
+
58
+ // Config
59
+ import type { ConfigTypes } from "azurajs/config";
60
+
61
+ // Router
62
+ import { Router } from "azurajs/router";
63
+ ```
64
+
34
65
  ## Quick Start
35
66
 
36
67
  ### 1. Create `azura.config.ts`
37
68
 
38
69
  ```typescript
39
- import type { ConfigTypes } from "azurajs";
70
+ import type { ConfigTypes } from "azurajs/config";
40
71
 
41
72
  const config: ConfigTypes = {
42
73
  environment: "development",
@@ -69,19 +100,10 @@ export default config;
69
100
  ### 2. Create your server
70
101
 
71
102
  ```typescript
72
- import {
73
- AzuraClient,
74
- Controller,
75
- Get,
76
- Post,
77
- Body,
78
- Param,
79
- Query,
80
- Res,
81
- applyDecorators,
82
- createLoggingMiddleware
83
- } from "azurajs";
84
- import type { ResponseServer } from "azurajs";
103
+ import { AzuraClient, applyDecorators } from "azurajs";
104
+ import { Controller, Get, Post, Body, Param, Query, Res } from "azurajs/decorators";
105
+ import { createLoggingMiddleware } from "azurajs/middleware";
106
+ import type { ResponseServer } from "azurajs/types";
85
107
 
86
108
  @Controller("/api")
87
109
  class UserController {
@@ -122,6 +144,77 @@ await app.listen();
122
144
  bun run index.ts
123
145
  ```
124
146
 
147
+ ## Alternative: Use with Custom Servers
148
+
149
+ AzuraJS can be used with **any server** that supports the Web Fetch API, just like Hono! This includes Bun, Deno, Cloudflare Workers, and more.
150
+
151
+ ### Using with Bun.serve
152
+
153
+ ```typescript
154
+ import { AzuraClient } from "azurajs";
155
+
156
+ const app = new AzuraClient();
157
+
158
+ app.get("/", (req, res) => {
159
+ res.json({ message: "Hello from Bun!" });
160
+ });
161
+
162
+ // Use with Bun's native server
163
+ const server = Bun.serve({
164
+ port: 3000,
165
+ fetch: app.fetch.bind(app),
166
+ });
167
+
168
+ console.log(`Server running on http://localhost:${server.port}`);
169
+ ```
170
+
171
+ ### Using with Deno
172
+
173
+ ```typescript
174
+ import { AzuraClient } from "azurajs";
175
+
176
+ const app = new AzuraClient();
177
+
178
+ app.get("/", (req, res) => {
179
+ res.json({ message: "Hello from Deno!" });
180
+ });
181
+
182
+ // Use with Deno.serve
183
+ Deno.serve({ port: 3000 }, app.fetch.bind(app));
184
+ ```
185
+
186
+ ### Using with Cloudflare Workers
187
+
188
+ ```typescript
189
+ import { AzuraClient } from "azurajs";
190
+
191
+ const app = new AzuraClient();
192
+
193
+ app.get("/", (req, res) => {
194
+ res.json({ message: "Hello from Cloudflare!" });
195
+ });
196
+
197
+ // Export for Cloudflare Workers
198
+ export default {
199
+ fetch: app.fetch.bind(app),
200
+ };
201
+ ```
202
+
203
+ ### Using with Node.js HTTP
204
+
205
+ ```typescript
206
+ import { AzuraClient } from "azurajs";
207
+
208
+ const app = new AzuraClient();
209
+
210
+ app.get("/", (req, res) => {
211
+ res.json({ message: "Hello from Node.js!" });
212
+ });
213
+
214
+ // Built-in Node.js HTTP server
215
+ await app.listen(3000);
216
+ ```
217
+
125
218
  ## API Reference
126
219
 
127
220
  ### Decorators
@@ -247,7 +340,7 @@ res.location(url: string)
247
340
  ### Middleware
248
341
 
249
342
  ```typescript
250
- import { createLoggingMiddleware } from "azurajs";
343
+ import { createLoggingMiddleware } from "azurajs/middleware";
251
344
 
252
345
  const app = new AzuraClient();
253
346
 
@@ -314,8 +407,9 @@ type ConfigTypes = {
314
407
  ### Complete CRUD API
315
408
 
316
409
  ```typescript
317
- import { AzuraClient, Controller, Get, Post, Put, Delete, Body, Param, Res, applyDecorators } from "azurajs";
318
- import type { ResponseServer } from "azurajs";
410
+ import { AzuraClient, applyDecorators } from "azurajs";
411
+ import { Controller, Get, Post, Put, Delete, Body, Param, Res } from "azurajs/decorators";
412
+ import type { ResponseServer } from "azurajs/types";
319
413
 
320
414
  interface User {
321
415
  id: number;
@@ -461,12 +555,9 @@ AzuraJS is designed for high performance:
461
555
  Full TypeScript support with complete type definitions:
462
556
 
463
557
  ```typescript
464
- import type {
465
- RequestServer,
466
- ResponseServer,
467
- ConfigTypes,
468
- RequestHandler
469
- } from "azurajs";
558
+ import type { RequestServer, ResponseServer } from "azurajs/types";
559
+ import type { ConfigTypes } from "azurajs/config";
560
+ import type { RequestHandler } from "azurajs/types";
470
561
  ```
471
562
 
472
563
  > ⚠️ Azura is TypeScript-only.
@@ -491,7 +582,7 @@ MIT License - see LICENSE file for details
491
582
 
492
583
  - [GitHub Repository](https://github.com/0xviny/azurajs)
493
584
  - [NPM Package](https://www.npmjs.com/package/azurajs)
494
- - [Documentation](https://azurajs.dev)
585
+ - [Documentation](https://azura.js.org/docs/en)
495
586
  - [Examples](https://github.com/0xviny/azurajs/tree/main/examples)
496
587
 
497
588
  ## Support
package/package.json CHANGED
@@ -1,16 +1,136 @@
1
1
  {
2
2
  "name": "azurajs",
3
- "version": "1.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Modern TypeScript-first web framework with decorator-based routing, zero dependencies, and built for performance",
5
- "main": "src/index.ts",
6
- "module": "src/index.ts",
7
- "types": "src/index.ts",
5
+ "main": "./src/index.ts",
6
+ "module": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
8
  "type": "module",
9
9
  "exports": {
10
10
  ".": {
11
+ "types": "./src/index.ts",
11
12
  "import": "./src/index.ts",
12
- "require": "./src/index.ts",
13
- "types": "./src/index.ts"
13
+ "default": "./src/index.ts"
14
+ },
15
+ "./decorators": {
16
+ "types": "./src/decorators/index.ts",
17
+ "import": "./src/decorators/index.ts",
18
+ "default": "./src/decorators/index.ts"
19
+ },
20
+ "./middleware": {
21
+ "types": "./src/middleware/index.ts",
22
+ "import": "./src/middleware/index.ts",
23
+ "default": "./src/middleware/index.ts"
24
+ },
25
+ "./types": {
26
+ "types": "./src/types/index.ts",
27
+ "import": "./src/types/index.ts",
28
+ "default": "./src/types/index.ts"
29
+ },
30
+ "./infra": {
31
+ "types": "./src/infra/index.ts",
32
+ "import": "./src/infra/index.ts",
33
+ "default": "./src/infra/index.ts"
34
+ },
35
+ "./router": {
36
+ "types": "./src/infra/Router.ts",
37
+ "import": "./src/infra/Router.ts",
38
+ "default": "./src/infra/Router.ts"
39
+ },
40
+ "./config": {
41
+ "types": "./src/shared/config/index.ts",
42
+ "import": "./src/shared/config/index.ts",
43
+ "default": "./src/shared/config/index.ts"
44
+ },
45
+ "./plugins": {
46
+ "types": "./src/shared/plugins/index.ts",
47
+ "import": "./src/shared/plugins/index.ts",
48
+ "default": "./src/shared/plugins/index.ts"
49
+ },
50
+ "./cors": {
51
+ "types": "./src/shared/plugins/CORSPlugin.ts",
52
+ "import": "./src/shared/plugins/CORSPlugin.ts",
53
+ "default": "./src/shared/plugins/CORSPlugin.ts"
54
+ },
55
+ "./rate-limit": {
56
+ "types": "./src/shared/plugins/RateLimitPlugin.ts",
57
+ "import": "./src/shared/plugins/RateLimitPlugin.ts",
58
+ "default": "./src/shared/plugins/RateLimitPlugin.ts"
59
+ },
60
+ "./utils": {
61
+ "types": "./src/utils/index.ts",
62
+ "import": "./src/utils/index.ts",
63
+ "default": "./src/utils/index.ts"
64
+ },
65
+ "./cookies": {
66
+ "types": "./src/utils/cookies/index.ts",
67
+ "import": "./src/utils/cookies/index.ts",
68
+ "default": "./src/utils/cookies/index.ts"
69
+ },
70
+ "./validators": {
71
+ "types": "./src/utils/validators/index.ts",
72
+ "import": "./src/utils/validators/index.ts",
73
+ "default": "./src/utils/validators/index.ts"
74
+ },
75
+ "./logger": {
76
+ "types": "./src/utils/Logger.ts",
77
+ "import": "./src/utils/Logger.ts",
78
+ "default": "./src/utils/Logger.ts"
79
+ },
80
+ "./http-error": {
81
+ "types": "./src/infra/utils/HttpError.ts",
82
+ "import": "./src/infra/utils/HttpError.ts",
83
+ "default": "./src/infra/utils/HttpError.ts"
84
+ },
85
+ "./package.json": "./package.json"
86
+ },
87
+ "typesVersions": {
88
+ "*": {
89
+ "*": [
90
+ "./src/*"
91
+ ],
92
+ "decorators": [
93
+ "./src/decorators/index.ts"
94
+ ],
95
+ "middleware": [
96
+ "./src/middleware/index.ts"
97
+ ],
98
+ "types": [
99
+ "./src/types/index.ts"
100
+ ],
101
+ "infra": [
102
+ "./src/infra/index.ts"
103
+ ],
104
+ "router": [
105
+ "./src/infra/Router.ts"
106
+ ],
107
+ "config": [
108
+ "./src/shared/config/index.ts"
109
+ ],
110
+ "plugins": [
111
+ "./src/shared/plugins/index.ts"
112
+ ],
113
+ "cors": [
114
+ "./src/shared/plugins/CORSPlugin.ts"
115
+ ],
116
+ "rate-limit": [
117
+ "./src/shared/plugins/RateLimitPlugin.ts"
118
+ ],
119
+ "utils": [
120
+ "./src/utils/index.ts"
121
+ ],
122
+ "cookies": [
123
+ "./src/utils/cookies/index.ts"
124
+ ],
125
+ "validators": [
126
+ "./src/utils/validators/index.ts"
127
+ ],
128
+ "logger": [
129
+ "./src/utils/Logger.ts"
130
+ ],
131
+ "http-error": [
132
+ "./src/infra/utils/HttpError.ts"
133
+ ]
14
134
  }
15
135
  },
16
136
  "files": [
@@ -34,23 +34,31 @@ export class Router {
34
34
  find(method: string, path: string): MatchResult {
35
35
  const segments = path.split("/").filter(Boolean);
36
36
  let node = this.root;
37
-
38
37
  const params: Record<string, string> = {};
39
- for (const seg of segments) {
40
- if (node.children.has(seg)) {
41
- node = node.children.get(seg)!;
42
- } else if (node.children.has(":")) {
43
- node = node.children.get(":")!;
44
-
45
- if (node.isParam && node.paramName) {
46
- params[node.paramName] = seg;
47
- }
38
+
39
+ for (let i = 0; i < segments.length; i++) {
40
+ const seg = segments[i];
41
+ let child = node.children.get(seg);
42
+
43
+ if (child) {
44
+ node = child;
48
45
  } else {
49
- throw new HttpError(404, "Route not found");
46
+ child = node.children.get(":");
47
+ if (child) {
48
+ node = child;
49
+ if (node.paramName) {
50
+ params[node.paramName] = seg;
51
+ }
52
+ } else {
53
+ throw new HttpError(404, "Route not found");
54
+ }
50
55
  }
51
56
  }
52
57
 
53
58
  const handlers = node.handlers.get(method.toUpperCase()) as Handler[];
59
+ if (!handlers) {
60
+ throw new HttpError(404, "Route not found");
61
+ }
54
62
  return { handlers, params };
55
63
  }
56
64
  }
@@ -114,6 +114,213 @@ export class AzuraClient {
114
114
  logger("info", `[${who}] listening on http://localhost:${port}`);
115
115
  if (this.opts.server?.ipHost) getIP(port);
116
116
  });
117
+
118
+ return this.server;
119
+ }
120
+
121
+ /**
122
+ * Fetch handler compatible with Web API (Bun, Deno, Cloudflare Workers, etc.)
123
+ * @example
124
+ * ```typescript
125
+ * const app = new AzuraClient();
126
+ * app.get('/', (req, res) => res.text('Hello World!'));
127
+ *
128
+ * // Use with Bun
129
+ * Bun.serve({
130
+ * port: 3000,
131
+ * fetch: app.fetch.bind(app),
132
+ * });
133
+ *
134
+ * // Use with Deno
135
+ * Deno.serve({ port: 3000 }, app.fetch.bind(app));
136
+ * ```
137
+ */
138
+ public async fetch(request: Request): Promise<Response> {
139
+ await this.initPromise;
140
+
141
+ const url = new URL(request.url);
142
+ const urlPath = url.pathname;
143
+
144
+ const safeQuery: Record<string, string> = {};
145
+ if (url.search) {
146
+ const rawQuery = parseQS(url.search.slice(1));
147
+ for (const k in rawQuery) {
148
+ const v = rawQuery[k];
149
+ safeQuery[k] = Array.isArray(v) ? v[0] || "" : v as string;
150
+ }
151
+ }
152
+
153
+ const cookieHeader = request.headers.get("cookie") || "";
154
+ const cookies = parseCookiesHeader(cookieHeader);
155
+
156
+ let body: any = {};
157
+ if (["POST", "PUT", "PATCH"].includes(request.method.toUpperCase())) {
158
+ const contentType = request.headers.get("content-type") || "";
159
+ try {
160
+ if (contentType.includes("application/json")) {
161
+ body = await request.json();
162
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
163
+ const text = await request.text();
164
+ const parsed = parseQS(text || "");
165
+ const b: Record<string, string> = {};
166
+ for (const k in parsed) {
167
+ const v = parsed[k];
168
+ b[k] = Array.isArray(v) ? v[0] || "" : (v as string) || "";
169
+ }
170
+ body = b;
171
+ } else {
172
+ body = await request.text();
173
+ }
174
+ } catch {
175
+ body = {};
176
+ }
177
+ }
178
+
179
+ const protocol = url.protocol.slice(0, -1) as "http" | "https";
180
+
181
+ const headersObj: Record<string, string | string[]> = {};
182
+ request.headers.forEach((value, key) => {
183
+ headersObj[key] = value;
184
+ });
185
+
186
+ const rawReq: Partial<RequestServer> = {
187
+ method: request.method,
188
+ url: url.pathname + url.search,
189
+ originalUrl: url.pathname + url.search,
190
+ path: urlPath || "/",
191
+ protocol,
192
+ secure: url.protocol === "https:",
193
+ hostname: url.hostname,
194
+ subdomains: url.hostname ? url.hostname.split(".").slice(0, -2) : [],
195
+ query: safeQuery,
196
+ cookies,
197
+ params: {},
198
+ body,
199
+ headers: headersObj as any,
200
+ get: (name: string) => request.headers.get(name.toLowerCase()) || undefined,
201
+ header: (name: string) => request.headers.get(name.toLowerCase()) || undefined,
202
+ ip: request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "",
203
+ ips: request.headers.get("x-forwarded-for")?.split(/\s*,\s*/) || [],
204
+ };
205
+
206
+ let statusCode = 200;
207
+ const responseHeaders = new Headers();
208
+ let responseBody: any = null;
209
+
210
+ const rawRes: Partial<ResponseServer> = {
211
+ statusCode,
212
+ status: (code: number) => {
213
+ statusCode = code;
214
+ return rawRes as ResponseServer;
215
+ },
216
+ set: (field: string, value: string | number | string[]) => {
217
+ responseHeaders.set(field, String(value));
218
+ return rawRes as ResponseServer;
219
+ },
220
+ header: (field: string, value: string | number | string[]) => {
221
+ responseHeaders.set(field, String(value));
222
+ return rawRes as ResponseServer;
223
+ },
224
+ get: (field: string) => responseHeaders.get(field) || undefined,
225
+ type: (t: string) => {
226
+ responseHeaders.set("Content-Type", t);
227
+ return rawRes as ResponseServer;
228
+ },
229
+ contentType: (t: string) => {
230
+ responseHeaders.set("Content-Type", t);
231
+ return rawRes as ResponseServer;
232
+ },
233
+ location: (u: string) => {
234
+ responseHeaders.set("Location", u);
235
+ return rawRes as ResponseServer;
236
+ },
237
+ redirect: ((a: number | string, b?: string) => {
238
+ if (typeof a === "number") {
239
+ statusCode = a;
240
+ responseHeaders.set("Location", b!);
241
+ } else {
242
+ statusCode = 302;
243
+ responseHeaders.set("Location", a);
244
+ }
245
+ return rawRes as ResponseServer;
246
+ }) as ResponseServer["redirect"],
247
+ cookie: (name: string, val: string, opts: CookieOptions = {}) => {
248
+ const s = serializeCookie(name, val, opts);
249
+ const prev = responseHeaders.get("Set-Cookie");
250
+ if (prev) {
251
+ responseHeaders.append("Set-Cookie", s);
252
+ } else {
253
+ responseHeaders.set("Set-Cookie", s);
254
+ }
255
+ return rawRes as ResponseServer;
256
+ },
257
+ clearCookie: (name: string, opts: CookieOptions = {}) => {
258
+ return rawRes.cookie!(name, "", { ...opts, expires: new Date(1), maxAge: 0 });
259
+ },
260
+ send: (b: any) => {
261
+ if (b === undefined || b === null) {
262
+ responseBody = "";
263
+ } else if (typeof b === "object") {
264
+ responseHeaders.set("Content-Type", "application/json");
265
+ responseBody = JSON.stringify(b);
266
+ } else {
267
+ responseBody = String(b);
268
+ }
269
+ return rawRes as ResponseServer;
270
+ },
271
+ json: (b: any) => {
272
+ responseHeaders.set("Content-Type", "application/json");
273
+ responseBody = JSON.stringify(b);
274
+ return rawRes as ResponseServer;
275
+ },
276
+ };
277
+
278
+ const errorHandler = (err: any) => {
279
+ statusCode = err instanceof HttpError ? err.status : 500;
280
+ responseHeaders.set("Content-Type", "application/json");
281
+ responseBody = JSON.stringify(
282
+ err instanceof HttpError
283
+ ? err.payload ?? { error: err.message || "Internal Server Error" }
284
+ : { error: err?.message || "Internal Server Error" }
285
+ );
286
+ };
287
+
288
+ try {
289
+ const { handlers, params } = this.router.find(request.method, urlPath || "/");
290
+ rawReq.params = params || {};
291
+
292
+ const chain = [
293
+ ...this.middlewares.map(adaptRequestHandler),
294
+ ...handlers.map(adaptRequestHandler),
295
+ ];
296
+
297
+ let idx = 0;
298
+ const next = async (err?: any) => {
299
+ if (err) return errorHandler(err);
300
+ if (idx >= chain.length) return;
301
+ const fn = chain[idx++];
302
+ try {
303
+ await fn({
304
+ request: rawReq as RequestServer,
305
+ response: rawRes as ResponseServer,
306
+ req: rawReq as RequestServer,
307
+ res: rawRes as ResponseServer,
308
+ next,
309
+ });
310
+ } catch (e) {
311
+ return errorHandler(e);
312
+ }
313
+ };
314
+
315
+ await next();
316
+ } catch (err) {
317
+ errorHandler(err);
318
+ }
319
+
320
+ return new Response(responseBody, {
321
+ status: statusCode,
322
+ headers: responseHeaders,
323
+ });
117
324
  }
118
325
 
119
326
  private async handle(rawReq: RequestServer, rawRes: ResponseServer) {
@@ -0,0 +1,3 @@
1
+ export { Server } from "./Server";
2
+ export { Router } from "./Router";
3
+ export { HttpError } from "./utils/HttpError";
@@ -0,0 +1,2 @@
1
+ export { ConfigModule } from "./ConfigModule";
2
+ export type { ConfigTypes } from "./ConfigModule";
@@ -0,0 +1,2 @@
1
+ export { cors } from "./CORSPlugin";
2
+ export { rateLimit } from "./RateLimitPlugin";
@@ -0,0 +1,5 @@
1
+ export type { RequestServer } from "./http/request.type";
2
+ export type { ResponseServer } from "./http/response.type";
3
+ export type { RequestHandler, HttpContext } from "./common.type";
4
+ export type { RouteDefinition, ParamDefinition, ParamSource } from "./routes.type";
5
+ export type { Schema as ValidationSchema } from "./validations.type";
@@ -1,16 +1,28 @@
1
1
  export function parseQS(qs: string): Record<string, string | string[]> {
2
2
  const out: Record<string, string | string[]> = {};
3
3
  if (!qs) return out;
4
- const parts = qs.replace(/^\?/, "").split("&");
5
- for (const p of parts) {
4
+
5
+ const parts = qs.split("&");
6
+ for (let i = 0; i < parts.length; i++) {
7
+ const p = parts[i];
6
8
  if (!p) continue;
9
+
7
10
  const idx = p.indexOf("=");
8
- const k = idx === -1 ? decodeURIComponent(p) : decodeURIComponent(p.slice(0, idx));
9
- const v = idx === -1 ? "" : decodeURIComponent(p.slice(idx + 1));
10
- if (Object.prototype.hasOwnProperty.call(out, k)) {
11
- const cur = out[k];
12
- if (Array.isArray(cur)) cur.push(v);
13
- else out[k] = [cur as string, v];
11
+ if (idx === -1) {
12
+ out[decodeURIComponent(p)] = "";
13
+ continue;
14
+ }
15
+
16
+ const k = decodeURIComponent(p.slice(0, idx));
17
+ const v = decodeURIComponent(p.slice(idx + 1));
18
+
19
+ const existing = out[k];
20
+ if (existing !== undefined) {
21
+ if (Array.isArray(existing)) {
22
+ existing.push(v);
23
+ } else {
24
+ out[k] = [existing as string, v];
25
+ }
14
26
  } else {
15
27
  out[k] = v;
16
28
  }
@@ -0,0 +1,2 @@
1
+ export { parseCookiesHeader } from "./ParserCookie";
2
+ export { serializeCookie } from "./SerializeCookie";
@@ -0,0 +1,4 @@
1
+ export { logger } from "./Logger";
2
+ export { parseQS } from "./Parser";
3
+ export * from "./cookies";
4
+ export * from "./validators";
@@ -0,0 +1,2 @@
1
+ export { validateDto, getDtoValidators } from "./DTOValidator";
2
+ export { validateSchema } from "./SchemaValidator";