@spikard/node 0.13.0 → 0.15.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,85 +1,91 @@
1
- <!-- GENERATED FILE — DO NOT EDIT DIRECTLY. Run: task readme:generate -->
2
-
3
- # Spikard for Node.js
1
+ # Spikard
4
2
 
5
3
  <div align="center" style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;">
6
- <a href="https://spikard.dev">
7
- <img src="https://img.shields.io/badge/docs-spikard.dev-007ec6" alt="Documentation">
8
- </a>
4
+ <!-- Language Bindings -->
9
5
  <a href="https://crates.io/crates/spikard">
10
- <img src="https://img.shields.io/crates/v/spikard.svg?color=007ec6" alt="Crates.io">
6
+ <img src="https://img.shields.io/crates/v/spikard?label=Rust&color=007ec6" alt="Rust">
11
7
  </a>
12
8
  <a href="https://pypi.org/project/spikard/">
13
- <img src="https://img.shields.io/pypi/v/spikard.svg?color=007ec6" alt="PyPI">
9
+ <img src="https://img.shields.io/pypi/v/spikard?label=Python&color=007ec6" alt="Python">
14
10
  </a>
15
11
  <a href="https://www.npmjs.com/package/@spikard/node">
16
- <img src="https://img.shields.io/npm/v/@spikard/node.svg?color=007ec6" alt="npm">
12
+ <img src="https://img.shields.io/npm/v/@spikard/node?label=Node.js&color=007ec6" alt="Node.js">
13
+ </a>
14
+ <a href="https://www.npmjs.com/package/@spikard/wasm">
15
+ <img src="https://img.shields.io/npm/v/@spikard/wasm?label=WASM&color=007ec6" alt="WASM">
17
16
  </a>
18
17
  <a href="https://rubygems.org/gems/spikard">
19
- <img src="https://img.shields.io/gem/v/spikard.svg?color=007ec6" alt="RubyGems">
18
+ <img src="https://img.shields.io/gem/v/spikard?label=Ruby&color=007ec6" alt="Ruby">
20
19
  </a>
21
20
  <a href="https://packagist.org/packages/spikard/spikard">
22
- <img src="https://img.shields.io/packagist/v/spikard/spikard.svg?color=007ec6" alt="Packagist">
21
+ <img src="https://img.shields.io/packagist/v/spikard/spikard?label=PHP&color=007ec6" alt="PHP">
23
22
  </a>
24
23
  <a href="https://hex.pm/packages/spikard">
25
- <img src="https://img.shields.io/hexpm/v/spikard.svg?color=007ec6" alt="Hex.pm">
24
+ <img src="https://img.shields.io/hexpm/v/spikard?label=Elixir&color=007ec6" alt="Elixir">
25
+ </a>
26
+ <a href="https://central.sonatype.com/artifact/dev.spikard/spikard">
27
+ <img src="https://img.shields.io/maven-central/v/dev.spikard/spikard?label=Java&color=007ec6" alt="Java">
28
+ </a>
29
+ <a href="https://github.com/Goldziher/spikard/releases">
30
+ <img src="https://img.shields.io/github/v/tag/Goldziher/spikard?label=Go&color=007ec6" alt="Go">
31
+ </a>
32
+ <a href="https://www.nuget.org/packages/Spikard/">
33
+ <img src="https://img.shields.io/nuget/v/Spikard?label=C%23&color=007ec6" alt="C#">
26
34
  </a>
35
+
36
+ <!-- Project Info -->
27
37
  <a href="https://github.com/Goldziher/spikard/blob/main/LICENSE">
28
- <img src="https://img.shields.io/badge/license-MIT-007ec6" alt="License">
38
+ <img src="https://img.shields.io/badge/License-MIT-007ec6" alt="License">
39
+ </a>
40
+ <a href="https://github.com/Goldziher/spikard">
41
+ <img src="https://img.shields.io/badge/docs-GitHub-007ec6" alt="Documentation">
29
42
  </a>
30
43
  </div>
31
44
 
32
- 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.
33
-
34
- ## Features
35
- - **Rust-Powered Performance**: Native speed via Tokio with dedicated thread pool
36
- - **Full TypeScript Support**: Auto-generated types from napi-rs FFI bindings
37
- - **Zero-Copy JSON**: Direct conversion without serialization overhead
38
- - **Tower-HTTP Middleware**: Compression, rate limiting, timeouts, auth, CORS, request IDs
39
- - **Schema Validation**: Zod integration for request/response validation
40
- - **Lifecycle Hooks**: onRequest, preValidation, preHandler, onResponse, onError
41
- - **Testing**: TestClient for HTTP, WebSocket, and SSE assertions
45
+ Rust-centric polyglot HTTP framework with OpenAPI/AsyncAPI/GraphQL/JSON-RPC codegen, tower-http middleware, and fixture-driven cross-language testing. Native NAPI-RS bindings for Node.js with TypeScript type definitions.
42
46
 
43
47
  ## Installation
44
48
 
49
+ **npm:**
50
+
45
51
  ```bash
46
52
  npm install @spikard/node
47
- # or
53
+ ```
54
+
55
+ **pnpm:**
56
+
57
+ ```bash
48
58
  pnpm add @spikard/node
49
59
  ```
50
60
 
51
- **Requirements:** Node.js 20+
61
+ **yarn:**
62
+
63
+ ```bash
64
+ yarn add @spikard/node
65
+ ```
52
66
 
53
- For building from source, see the [main README](../../README.md#development).
67
+ ### System Requirements
68
+
69
+ - **Node.js 18+** required (NAPI-RS native bindings)
70
+ - Pre-built binaries for Linux (x86_64), macOS (arm64, x86_64), Windows (x86_64)
54
71
 
55
72
  ## Quick Start
56
73
 
57
74
  ```typescript
58
- import { Spikard, type Request } from "@spikard/node";
75
+ import { Spikard, type Request } from "spikard";
59
76
  import { z } from "zod";
60
77
 
61
- const UserSchema = z.object({
62
- id: z.number(),
63
- name: z.string(),
64
- email: z.string().email(),
65
- });
66
-
78
+ const UserSchema = z.object({ id: z.number(), name: z.string() });
67
79
  type User = z.infer<typeof UserSchema>;
68
80
 
69
81
  const app = new Spikard();
70
82
 
71
- const getUser = async (req: Request): Promise<User> => {
72
- const id = Number(req.params["id"] ?? 0);
73
- return { id, name: "Alice", email: "alice@example.com" };
74
- };
75
-
76
- const createUser = async (req: Request): Promise<User> => {
77
- return UserSchema.parse(req.json());
78
- };
79
-
80
83
  app.addRoute(
81
84
  { method: "GET", path: "/users/:id", handler_name: "getUser", is_async: true },
82
- getUser,
85
+ async (req: Request): Promise<User> => {
86
+ const id = Number(req.params["id"] ?? 0);
87
+ return { id, name: "Alice" };
88
+ },
83
89
  );
84
90
 
85
91
  app.addRoute(
@@ -91,31 +97,42 @@ app.addRoute(
91
97
  response_schema: UserSchema,
92
98
  is_async: true,
93
99
  },
94
- createUser,
100
+ async (req: Request): Promise<User> => UserSchema.parse(req.json()),
95
101
  );
96
102
 
97
- app.run({ port: 8000 });
103
+ if (require.main === module) {
104
+ app.run({ port: 8000 });
105
+ }
98
106
  ```
99
- ## Routing & Schemas
100
107
 
101
- Routes support Zod validation (recommended) or raw JSON Schema:
108
+ ## Features
109
+
110
+ - **HTTP routing** — type-safe route definitions with path, query, and body parameter validation
111
+ - **OpenAPI / AsyncAPI / GraphQL / JSON-RPC** — code generation and spec parsing built in
112
+ - **Tower middleware** — compression, rate limiting, timeouts, auth (JWT/API key), static files
113
+ - **Lifecycle hooks** — `onRequest`, `preValidation`, `preHandler`, `onResponse`, `onError`
114
+ - **Fixture-driven testing** — shared JSON fixtures drive tests across all language bindings
115
+ - **Polyglot** — single Rust core, thin bindings for Python, Node.js, Ruby, PHP, Elixir, Go, Java, C#, Kotlin, Dart, Gleam, WASM, Swift, Zig, and C FFI
116
+
117
+ ## Routing
102
118
 
103
119
  ```typescript
104
- import { Spikard, type Request } from "@spikard/node";
120
+ import { Spikard, type Request } from "spikard";
105
121
  import { z } from "zod";
106
122
 
123
+ const UserSchema = z.object({ id: z.number(), name: z.string() });
124
+ type User = z.infer<typeof UserSchema>;
125
+
107
126
  const app = new Spikard();
108
127
 
109
- const UserSchema = z.object({
110
- name: z.string().min(1),
111
- email: z.string().email(),
112
- });
128
+ const health = async (): Promise<{ status: string }> => ({ status: "ok" });
113
129
 
114
- const createUser = async (req: Request) => {
115
- const user = req.json();
116
- return { id: 1, ...user };
130
+ const createUser = async (req: Request): Promise<User> => {
131
+ return UserSchema.parse(req.json());
117
132
  };
118
133
 
134
+ app.addRoute({ method: "GET", path: "/health", handler_name: "health", is_async: true }, health);
135
+
119
136
  app.addRoute(
120
137
  {
121
138
  method: "POST",
@@ -129,171 +146,60 @@ app.addRoute(
129
146
  );
130
147
  ```
131
148
 
132
- Supported HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE.
149
+ ## Validation
133
150
 
134
- ## Dependency Injection
151
+ ```typescript
152
+ import { Spikard, type Request } from "spikard";
153
+ import { z } from "zod";
135
154
 
136
- Register values or factories and access them via `request.dependencies`:
155
+ const PaymentSchema = z.object({
156
+ id: z.string().uuid(),
157
+ amount: z.number().positive(),
158
+ });
159
+ type Payment = z.infer<typeof PaymentSchema>;
137
160
 
138
- ```typescript
139
161
  const app = new Spikard();
140
162
 
141
- app.provide("config", { dbUrl: "postgresql://localhost/app" });
142
- app.provide(
143
- "dbPool",
144
- async ({ config }) => ({ url: config.dbUrl, driver: "pool" }),
145
- { dependsOn: ["config"], singleton: true },
146
- );
163
+ const createPayment = async (req: Request): Promise<Payment> => {
164
+ return PaymentSchema.parse(req.json());
165
+ };
147
166
 
148
167
  app.addRoute(
149
- { method: "GET", path: "/stats", handler_name: "stats", is_async: true },
150
- async (req) => {
151
- const deps = req.dependencies ?? {};
152
- return { db: deps.dbPool?.url, env: deps.config?.dbUrl };
168
+ {
169
+ method: "POST",
170
+ path: "/payments",
171
+ handler_name: "createPayment",
172
+ request_schema: PaymentSchema,
173
+ response_schema: PaymentSchema,
174
+ is_async: true,
153
175
  },
176
+ createPayment,
154
177
  );
155
178
  ```
156
179
 
157
- ## Request Handling
158
-
159
- Access query, path params, headers, cookies, and body:
160
-
161
- ```typescript
162
- get("/search")(async (req) => {
163
- const q = req.query.q;
164
- const id = req.params.id;
165
- const auth = req.headers.authorization;
166
- const session = req.cookies.session_id;
167
- const body = req.json<{ name: string }>();
168
- const form = req.form();
169
- return { query: q, id };
170
- });
171
- ```
172
-
173
- ## Advanced Features
180
+ ## Middleware
174
181
 
175
- **File Uploads:**
176
182
  ```typescript
177
- post("/upload")(async (req) => {
178
- const body = req.json<{ file: UploadFile }>();
179
- return { filename: body.file.filename, size: body.file.size };
180
- });
181
- ```
183
+ import { Spikard, type Request } from "spikard";
182
184
 
183
- **Streaming Responses:**
184
- ```typescript
185
- get("/stream")(async function* () {
186
- for (let i = 0; i < 10; i++) {
187
- yield JSON.stringify({ count: i }) + "\n";
188
- await new Promise(r => setTimeout(r, 100));
189
- }
190
- });
191
- ```
192
-
193
- ## Configuration
194
-
195
- Configure middleware, compression, rate limiting, and authentication:
196
-
197
- ```typescript
198
- const config: ServerConfig = {
199
- port: 8080,
200
- workers: 4,
201
- maxBodySize: 10 * 1024 * 1024,
202
- requestTimeout: 30,
203
- compression: { gzip: true, brotli: true, minSize: 1024 },
204
- rateLimit: { perSecond: 100, burst: 200 },
205
- jwtAuth: { secret: "key", algorithm: "HS256" },
206
- };
207
-
208
- app.run(config);
209
- ```
210
-
211
- See [ServerConfig](../../docs/adr/0002-runtime-and-middleware.md) for all options.
212
-
213
- ## Lifecycle Hooks
214
-
215
- Execute code at key request/response stages:
185
+ const app = new Spikard();
216
186
 
217
- ```typescript
218
- app.onRequest(async (request) => {
187
+ app.onRequest(async (request: Request): Promise<Request> => {
219
188
  console.log(`${request.method} ${request.path}`);
220
189
  return request;
221
190
  });
222
-
223
- app.preValidation(async (request) => {
224
- if (!request.headers["authorization"]) {
225
- return { status: 401, body: { error: "Unauthorized" } };
226
- }
227
- return request;
228
- });
229
-
230
- app.onResponse(async (response) => {
231
- response.headers["X-Frame-Options"] = "DENY";
232
- return response;
233
- });
234
- ```
235
-
236
- ## Performance
237
-
238
- Benchmarked across 34 workloads at 100 concurrency ([methodology](../../docs/benchmarks/methodology.md)):
239
-
240
- | Framework | Avg RPS | P50 (ms) | P99 (ms) |
241
- |-----------|--------:|----------:|----------:|
242
- | **spikard (Bun)** | 49,460 | 2.18 | 4.21 |
243
- | **spikard (Node)** | 46,160 | 2.18 | 3.35 |
244
- | elysia | 44,326 | 2.41 | 4.68 |
245
- | kito | 36,958 | 4.94 | 12.86 |
246
- | fastify | 19,167 | 6.74 | 14.76 |
247
- | morojs | 14,196 | 6.44 | 12.61 |
248
- | hono | 10,928 | 10.91 | 18.62 |
249
-
250
- Spikard is **1.2x faster than Kito and 2.4x faster than Fastify**.
251
-
252
- Key optimizations:
253
- - **napi-rs** zero-copy FFI bindings
254
- - **Dedicated Tokio runtime** without blocking Node event loop
255
- - **Zero-copy JSON** conversion (30-40% faster than JSON.parse)
256
- - **ThreadsafeFunction** for async JavaScript callbacks
257
-
258
- ## Testing
259
-
260
- Use TestClient for HTTP, WebSocket, and SSE testing:
261
-
262
- ```typescript
263
- import { TestClient } from "@spikard/node";
264
- import { expect } from "vitest";
265
-
266
- const client = new TestClient(app);
267
-
268
- // HTTP testing
269
- const response = await client.get("/users/123");
270
- expect(response.statusCode).toBe(200);
271
-
272
- // WebSocket testing
273
- const ws = await client.websocketConnect("/ws");
274
- await ws.sendJson({ message: "hello" });
275
-
276
- // SSE testing
277
- const sse = await client.get("/events");
278
191
  ```
279
192
 
280
- ## Examples
281
-
282
- See [examples/](../../examples/) for runnable projects. Code generation is supported for OpenAPI, GraphQL, AsyncAPI, OpenRPC/JSON-RPC, and Protobuf/gRPC specifications.
283
-
284
193
  ## Documentation
285
194
 
286
- Full documentation at [spikard.dev](https://spikard.dev). See also [CONTRIBUTING.md](../../CONTRIBUTING.md).
195
+ - **[Repository](https://github.com/Goldziher/spikard)** source code, examples, and contributing guide
196
+ - **[Examples](https://github.com/Goldziher/spikard/tree/main/examples)** — working examples per language
197
+ - **[Issues](https://github.com/Goldziher/spikard/issues)** — bug reports and feature requests
287
198
 
288
- ## Other Languages
199
+ ## Contributing
289
200
 
290
- - **Rust:** [Crates.io](https://crates.io/crates/spikard)
291
- - **Python:** [PyPI](https://pypi.org/project/spikard/)
292
- - **TypeScript:** [npm (@spikard/node)](https://www.npmjs.com/package/@spikard/node)
293
- - **Ruby:** [RubyGems](https://rubygems.org/gems/spikard)
294
- - **PHP:** [Packagist](https://packagist.org/packages/spikard/spikard)
295
- - **Elixir:** [Hex.pm](https://hex.pm/packages/spikard)
201
+ Contributions are welcome. See [CONTRIBUTING.md](https://github.com/Goldziher/spikard/blob/main/CONTRIBUTING.md).
296
202
 
297
203
  ## License
298
204
 
299
- MIT - See [LICENSE](../../LICENSE) for details
205
+ MIT License see [LICENSE](https://github.com/Goldziher/spikard/blob/main/LICENSE) for details.