@spikard/node 0.9.1 → 0.10.1

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
@@ -1,39 +1,41 @@
1
- # spikard-node
1
+ # Spikard for Node.js
2
2
 
3
- High-performance Node.js bindings for Spikard HTTP framework via napi-rs.
3
+ [![Documentation](https://img.shields.io/badge/docs-spikard.dev-blue)](https://spikard.dev)
4
+ [![Crates.io](https://img.shields.io/crates/v/spikard.svg?color=blue)](https://crates.io/crates/spikard)
5
+ [![PyPI](https://img.shields.io/pypi/v/spikard.svg?color=blue)](https://pypi.org/project/spikard/)
6
+ [![npm](https://img.shields.io/npm/v/@spikard/node.svg?color=blue)](https://www.npmjs.com/package/@spikard/node)
7
+ [![Gem](https://img.shields.io/gem/v/spikard.svg?color=blue)](https://rubygems.org/gems/spikard)
8
+ [![Packagist](https://img.shields.io/packagist/v/spikard/spikard.svg?color=blue)](https://packagist.org/packages/spikard/spikard)
9
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../LICENSE)
4
10
 
5
- ## Status & Badges
11
+ High-performance HTTP framework for Node.js powered by a Rust core. Provides type-safe routing, validation, middleware, and testing via **napi-rs** bindings with zero-copy JSON conversion.
6
12
 
7
- [![npm](https://img.shields.io/npm/v/spikard.svg)](https://www.npmjs.com/package/spikard)
8
- [![npm downloads](https://img.shields.io/npm/dm/spikard.svg)](https://www.npmjs.com/package/spikard)
9
- [![Crates.io](https://img.shields.io/crates/v/spikard-node.svg)](https://crates.io/crates/spikard-node)
10
- [![Documentation](https://docs.rs/spikard-node/badge.svg)](https://docs.rs/spikard-node)
11
- [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
- [![Discord](https://img.shields.io/badge/Discord-Join%20our%20community-7289da)](https://discord.gg/pXxagNK2zN)
13
+ ## Features
13
14
 
14
- ## Overview
15
-
16
- High-performance TypeScript/Node.js web framework with a Rust core. Build REST APIs with type-safe routing backed by Axum and Tower-HTTP.
15
+ - **Rust-Powered Performance**: Native speed via Tokio with dedicated thread pool
16
+ - **Full TypeScript Support**: Auto-generated types from napi-rs FFI bindings
17
+ - **Zero-Copy JSON**: Direct conversion without serialization overhead
18
+ - **Tower-HTTP Middleware**: Compression, rate limiting, timeouts, auth, CORS, request IDs
19
+ - **Schema Validation**: Zod integration for request/response validation
20
+ - **Lifecycle Hooks**: onRequest, preValidation, preHandler, onResponse, onError
21
+ - **Testing**: TestClient for HTTP, WebSocket, and SSE assertions
17
22
 
18
23
  ## Installation
19
24
 
20
- **From source (currently):**
21
-
22
25
  ```bash
23
- cd packages/node
24
- pnpm install
25
- pnpm build
26
+ npm install @spikard/node
27
+ # or
28
+ pnpm add @spikard/node
26
29
  ```
27
30
 
28
- **Requirements:**
29
- - Node.js 20+
30
- - pnpm 10+
31
- - Rust toolchain (for building from source)
31
+ **Requirements:** Node.js 20+
32
+
33
+ For building from source, see the [main README](../../README.md#development).
32
34
 
33
35
  ## Quick Start
34
36
 
35
37
  ```typescript
36
- import { Spikard, type Request } from "spikard";
38
+ import { Spikard, type Request } from "@spikard/node";
37
39
  import { z } from "zod";
38
40
 
39
41
  const UserSchema = z.object({
@@ -56,12 +58,7 @@ const createUser = async (req: Request): Promise<User> => {
56
58
  };
57
59
 
58
60
  app.addRoute(
59
- {
60
- method: "GET",
61
- path: "/users/:id",
62
- handler_name: "getUser",
63
- is_async: true,
64
- },
61
+ { method: "GET", path: "/users/:id", handler_name: "getUser", is_async: true },
65
62
  getUser,
66
63
  );
67
64
 
@@ -77,261 +74,127 @@ app.addRoute(
77
74
  createUser,
78
75
  );
79
76
 
80
- if (require.main === module) {
81
- app.run({ port: 8000 });
82
- }
77
+ app.run({ port: 8000 });
83
78
  ```
84
79
 
85
- ## Route Registration
80
+ ## Routing & Schemas
86
81
 
87
- ### Manual Registration with `addRoute`
88
-
89
- Routes are registered manually using `app.addRoute(metadata, handler)`:
82
+ Routes support Zod validation (recommended) or raw JSON Schema:
90
83
 
91
84
  ```typescript
92
- import { Spikard, type Request } from "spikard";
85
+ import { Spikard, type Request } from "@spikard/node";
86
+ import { z } from "zod";
93
87
 
94
88
  const app = new Spikard();
95
89
 
96
- async function listUsers(_req: Request): Promise<{ users: unknown[] }> {
97
- return { users: [] };
98
- }
99
-
100
- async function createUser(_req: Request): Promise<{ created: boolean }> {
101
- return { created: true };
102
- }
90
+ const UserSchema = z.object({
91
+ name: z.string().min(1),
92
+ email: z.string().email(),
93
+ });
103
94
 
104
- app.addRoute(
105
- {
106
- method: "GET",
107
- path: "/users",
108
- handler_name: "listUsers",
109
- is_async: true,
110
- },
111
- listUsers
112
- );
95
+ const createUser = async (req: Request) => {
96
+ const user = req.json();
97
+ return { id: 1, ...user };
98
+ };
113
99
 
114
100
  app.addRoute(
115
101
  {
116
102
  method: "POST",
117
103
  path: "/users",
118
104
  handler_name: "createUser",
105
+ request_schema: UserSchema,
106
+ response_schema: UserSchema,
119
107
  is_async: true,
120
108
  },
121
- createUser
109
+ createUser,
122
110
  );
123
111
  ```
124
112
 
125
- ### Supported HTTP Methods
113
+ Supported HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE.
126
114
 
127
- - `GET` - Retrieve resources
128
- - `POST` - Create resources
129
- - `PUT` - Replace resources
130
- - `PATCH` - Update resources
131
- - `DELETE` - Delete resources
132
- - `HEAD` - Get headers only
133
- - `OPTIONS` - Get allowed methods
134
- - `TRACE` - Echo the request
115
+ ## Dependency Injection
135
116
 
136
- ### With Schemas
137
-
138
- Spikard supports **Zod schemas** and **raw JSON Schema objects**.
139
-
140
- **With Zod (recommended - type inference):**
117
+ Register values or factories and access them via `request.dependencies`:
141
118
 
142
119
  ```typescript
143
- import { post } from "spikard";
144
- import { z } from "zod";
145
-
146
- const CreateUserSchema = z.object({
147
- name: z.string().min(1),
148
- email: z.string().email(),
149
- age: z.number().int().min(18),
150
- });
151
-
152
- post("/users", {
153
- bodySchema: CreateUserSchema,
154
- responseSchema: z.object({ id: z.number(), name: z.string() }),
155
- })(async function createUser(req) {
156
- const user = req.json();
157
- return { id: 1, name: user.name };
158
- });
159
- ```
120
+ const app = new Spikard();
160
121
 
161
- **With raw JSON Schema:**
122
+ app.provide("config", { dbUrl: "postgresql://localhost/app" });
123
+ app.provide(
124
+ "dbPool",
125
+ async ({ config }) => ({ url: config.dbUrl, driver: "pool" }),
126
+ { dependsOn: ["config"], singleton: true },
127
+ );
162
128
 
163
- ```typescript
164
- const userSchema = {
165
- type: "object",
166
- properties: {
167
- name: { type: "string" },
168
- email: { type: "string", format: "email" },
129
+ app.addRoute(
130
+ { method: "GET", path: "/stats", handler_name: "stats", is_async: true },
131
+ async (req) => {
132
+ const deps = req.dependencies ?? {};
133
+ return { db: deps.dbPool?.url, env: deps.config?.dbUrl };
169
134
  },
170
- required: ["name", "email"],
171
- };
172
-
173
- post("/users", { bodySchema: userSchema })(async function createUser(req) {
174
- const user = req.json<{ name: string; email: string }>();
175
- return { id: 1, ...user };
176
- });
135
+ );
177
136
  ```
178
137
 
179
138
  ## Request Handling
180
139
 
181
- ### Accessing Request Data
140
+ Access query, path params, headers, cookies, and body:
182
141
 
183
142
  ```typescript
184
- get("/search")(async function search(req) {
185
- // Query parameters
186
- const params = new URLSearchParams(req.queryString);
187
- const q = params.get("q");
188
- const limit = params.get("limit") ?? "10";
189
-
190
- // Headers
191
- const auth = req.headers["authorization"];
192
-
193
- // Method and path
194
- console.log(`${req.method} ${req.path}`);
195
-
196
- return { query: q, limit: parseInt(limit) };
197
- });
198
- ```
199
-
200
- ### JSON Body
201
-
202
- ```typescript
203
- post("/users")(async function createUser(req) {
204
- const body = req.json<{ name: string; email: string }>();
205
- return { id: 1, ...body };
206
- });
207
- ```
208
-
209
- ### Form Data
210
-
211
- ```typescript
212
- post("/login")(async function login(req) {
143
+ get("/search")(async (req) => {
144
+ const q = req.query.q;
145
+ const id = req.params.id;
146
+ const auth = req.headers.authorization;
147
+ const session = req.cookies.session_id;
148
+ const body = req.json<{ name: string }>();
213
149
  const form = req.form();
214
- return {
215
- username: form.username,
216
- password: form.password,
217
- };
150
+ return { query: q, id };
218
151
  });
219
152
  ```
220
153
 
221
- ## Handler Wrappers
222
-
223
- For automatic parameter extraction:
224
-
225
- ```typescript
226
- import { wrapHandler, wrapBodyHandler } from "spikard";
227
-
228
- // Body-only wrapper
229
- post("/users", {}, wrapBodyHandler(async (body: CreateUserRequest) => {
230
- return { id: 1, name: body.name };
231
- }));
232
-
233
- // Full context wrapper
234
- get(
235
- "/users/:id",
236
- {},
237
- wrapHandler(async (params: { id: string }, query: Record<string, unknown>) => {
238
- return { id: Number(params.id), query };
239
- }),
240
- );
241
- ```
242
-
243
- ## File Uploads
154
+ ## Advanced Features
244
155
 
156
+ **File Uploads:**
245
157
  ```typescript
246
- import { UploadFile } from "spikard";
247
-
248
- interface UploadRequest {
249
- file: UploadFile;
250
- description: string;
251
- }
252
-
253
- post("/upload")(async function upload(req) {
254
- const body = req.json<UploadRequest>();
255
- const content = body.file.read();
256
-
257
- return {
258
- filename: body.file.filename,
259
- size: body.file.size,
260
- contentType: body.file.contentType,
261
- };
158
+ post("/upload")(async (req) => {
159
+ const body = req.json<{ file: UploadFile }>();
160
+ return { filename: body.file.filename, size: body.file.size };
262
161
  });
263
162
  ```
264
163
 
265
- ## Streaming Responses
266
-
164
+ **Streaming Responses:**
267
165
  ```typescript
268
- import { StreamingResponse } from "spikard";
269
-
270
- async function* generateData() {
166
+ get("/stream")(async function* () {
271
167
  for (let i = 0; i < 10; i++) {
272
168
  yield JSON.stringify({ count: i }) + "\n";
273
- await new Promise((resolve) => setTimeout(resolve, 100));
169
+ await new Promise(r => setTimeout(r, 100));
274
170
  }
275
- }
276
-
277
- get("/stream")(async function stream() {
278
- return new StreamingResponse(generateData(), {
279
- statusCode: 200,
280
- headers: { "Content-Type": "application/x-ndjson" },
281
- });
282
171
  });
283
172
  ```
284
173
 
285
174
  ## Configuration
286
175
 
287
- ```typescript
288
- import { Spikard, runServer, type ServerConfig } from "spikard";
289
-
290
- const app = new Spikard();
176
+ Configure middleware, compression, rate limiting, and authentication:
291
177
 
178
+ ```typescript
292
179
  const config: ServerConfig = {
293
- host: "0.0.0.0",
294
180
  port: 8080,
295
181
  workers: 4,
296
- enableRequestId: true,
297
- maxBodySize: 10 * 1024 * 1024, // 10 MB
298
- requestTimeout: 30, // seconds
299
- compression: {
300
- gzip: true,
301
- brotli: true,
302
- quality: 9,
303
- minSize: 1024,
304
- },
305
- rateLimit: {
306
- perSecond: 100,
307
- burst: 200,
308
- ipBased: true,
309
- },
310
- jwtAuth: {
311
- secret: "your-secret-key",
312
- algorithm: "HS256",
313
- },
314
- staticFiles: [
315
- {
316
- directory: "./public",
317
- routePrefix: "/static",
318
- indexFile: true,
319
- },
320
- ],
321
- openapi: {
322
- enabled: true,
323
- title: "My API",
324
- version: "1.0.0",
325
- swaggerUiPath: "/docs",
326
- redocPath: "/redoc",
327
- },
182
+ maxBodySize: 10 * 1024 * 1024,
183
+ requestTimeout: 30,
184
+ compression: { gzip: true, brotli: true, minSize: 1024 },
185
+ rateLimit: { perSecond: 100, burst: 200 },
186
+ jwtAuth: { secret: "key", algorithm: "HS256" },
328
187
  };
329
188
 
330
- runServer(app, config);
189
+ app.run(config);
331
190
  ```
332
191
 
192
+ See [ServerConfig](../../docs/adr/0002-runtime-and-middleware.md) for all options.
193
+
333
194
  ## Lifecycle Hooks
334
195
 
196
+ Execute code at key request/response stages:
197
+
335
198
  ```typescript
336
199
  app.onRequest(async (request) => {
337
200
  console.log(`${request.method} ${request.path}`);
@@ -339,171 +202,82 @@ app.onRequest(async (request) => {
339
202
  });
340
203
 
341
204
  app.preValidation(async (request) => {
342
- // Check before validation
343
205
  if (!request.headers["authorization"]) {
344
- return {
345
- status: 401,
346
- body: { error: "Unauthorized" },
347
- };
206
+ return { status: 401, body: { error: "Unauthorized" } };
348
207
  }
349
208
  return request;
350
209
  });
351
210
 
352
- app.preHandler(async (request) => {
353
- // After validation, before handler
354
- return request;
355
- });
356
-
357
211
  app.onResponse(async (response) => {
358
212
  response.headers["X-Frame-Options"] = "DENY";
359
213
  return response;
360
214
  });
361
-
362
- app.onError(async (response) => {
363
- console.error(`Error: ${response.status}`);
364
- return response;
365
- });
366
- ```
367
-
368
- ## Background Tasks
369
-
370
- ```typescript
371
- import * as background from "spikard/background";
372
-
373
- post("/process")(async function process(req) {
374
- const data = req.json();
375
-
376
- background.run(() => {
377
- // Heavy processing after response sent
378
- processData(data);
379
- });
380
-
381
- return { status: "processing" };
382
- });
383
215
  ```
384
216
 
385
217
  ## Testing
386
218
 
219
+ Use TestClient for HTTP, WebSocket, and SSE testing:
220
+
387
221
  ```typescript
388
- import { TestClient } from "spikard";
222
+ import { TestClient } from "@spikard/node";
389
223
  import { expect } from "vitest";
390
224
 
391
- const app = {
392
- routes: [
393
- /* ... */
394
- ],
395
- handlers: {
396
- /* ... */
397
- },
398
- };
399
-
400
225
  const client = new TestClient(app);
401
226
 
227
+ // HTTP testing
402
228
  const response = await client.get("/users/123");
403
229
  expect(response.statusCode).toBe(200);
404
- expect(response.json()).toEqual({ id: "123", name: "Alice" });
405
- ```
406
230
 
407
- ### WebSocket Testing
408
-
409
- ```typescript
231
+ // WebSocket testing
410
232
  const ws = await client.websocketConnect("/ws");
411
233
  await ws.sendJson({ message: "hello" });
412
- const response = await ws.receiveJson();
413
- expect(response.echo.message).toBe("hello");
414
- await ws.close();
415
- ```
416
-
417
- ### SSE Testing
418
234
 
419
- ```typescript
420
- const response = await client.get("/events");
421
- const sse = new SseStream(response.text());
422
- const events = sse.eventsAsJson();
423
- expect(events.length).toBeGreaterThan(0);
235
+ // SSE testing
236
+ const sse = await client.get("/events");
424
237
  ```
425
238
 
426
- ## Type Safety
427
-
428
- Full TypeScript support with auto-generated types:
429
-
430
- ```typescript
431
- import {
432
- type Request,
433
- type Response,
434
- type ServerConfig,
435
- type RouteOptions,
436
- type HandlerFunction,
437
- } from "spikard";
438
- ```
439
-
440
- ### Parameter Types
441
-
442
- ```typescript
443
- import { Query, Path, Body, QueryDefault } from "spikard";
444
-
445
- function handler(
446
- id: Path<number>,
447
- limit: Query<string | undefined>,
448
- body: Body<UserType>
449
- ) {
450
- // Full type inference
451
- }
452
- ```
453
-
454
- ## Validation with Zod
455
-
456
- ```typescript
457
- import { z } from "zod";
458
-
459
- const UserSchema = z.object({
460
- name: z.string().min(1).max(100),
461
- email: z.string().email(),
462
- age: z.number().int().min(18).optional(),
463
- tags: z.array(z.string()).default([]),
464
- });
465
-
466
- post("/users", { bodySchema: UserSchema })(async function createUser(req) {
467
- const user = req.json<z.infer<typeof UserSchema>>();
468
- // user is fully typed and validated
469
- return user;
470
- });
471
- ```
472
-
473
- ## Running the Server
474
-
475
- ```typescript
476
- // Simple start
477
- app.run({ port: 8000 });
239
+ ## Performance
478
240
 
479
- // With full configuration
480
- import { runServer } from "spikard";
241
+ Benchmarked across 34 workloads at 100 concurrency ([methodology](../../docs/benchmarks/methodology.md)):
481
242
 
482
- runServer(app, {
483
- host: "0.0.0.0",
484
- port: 8080,
485
- workers: 4,
486
- });
487
- ```
243
+ | Framework | Avg RPS | P50 (ms) | P99 (ms) |
244
+ |-----------|--------:|----------:|----------:|
245
+ | **spikard (Bun)** | 49,460 | 2.18 | 4.21 |
246
+ | **spikard (Node)** | 46,160 | 2.18 | 3.35 |
247
+ | elysia | 44,326 | 2.41 | 4.68 |
248
+ | kito | 36,958 | 4.94 | 12.86 |
249
+ | fastify | 19,167 | 6.74 | 14.76 |
250
+ | morojs | 14,196 | 6.44 | 12.61 |
251
+ | hono | 10,928 | 10.91 | 18.62 |
488
252
 
489
- ## Performance
253
+ Spikard Node is **1.2x faster** than Kito and **2.4x faster** than Fastify.
490
254
 
491
- Node.js bindings use:
492
- - **napi-rs** for zero-copy FFI
255
+ Key optimizations:
256
+ - **napi-rs** zero-copy FFI bindings
257
+ - **Dedicated Tokio runtime** without blocking Node event loop
258
+ - **Zero-copy JSON** conversion (30-40% faster than JSON.parse)
493
259
  - **ThreadsafeFunction** for async JavaScript callbacks
494
- - Dedicated Tokio runtime (doesn't block Node event loop)
495
- - Direct type conversion without JSON serialization overhead
496
260
 
497
261
  ## Examples
498
262
 
499
- See `/examples/node/` for more examples.
263
+ See [examples/](../../examples/) for runnable projects. Code generation is supported for OpenAPI, GraphQL, AsyncAPI, and JSON-RPC specifications.
500
264
 
501
265
  ## Documentation
502
266
 
503
- - [Main Project README](../../README.md)
504
- - [Contributing Guide](../../CONTRIBUTING.md)
505
- - [TypeScript API Reference](./src/index.ts)
267
+ Full documentation at [spikard.dev](https://spikard.dev). See also [CONTRIBUTING.md](../../CONTRIBUTING.md).
268
+
269
+ ## Ecosystem
270
+
271
+ Spikard is available across multiple languages:
272
+
273
+ | Platform | Package | Status |
274
+ |----------|---------|--------|
275
+ | **Node.js** | [@spikard/node](https://www.npmjs.com/package/@spikard/node) | Stable |
276
+ | **Python** | [spikard](https://pypi.org/project/spikard/) | Stable |
277
+ | **Rust** | [spikard](https://crates.io/crates/spikard) | Stable |
278
+ | **Ruby** | [spikard](https://rubygems.org/gems/spikard) | Stable |
279
+ | **PHP** | [spikard/spikard](https://packagist.org/packages/spikard/spikard) | Stable |
506
280
 
507
281
  ## License
508
282
 
509
- MIT
283
+ MIT - See [LICENSE](../../LICENSE) for details