@spikard/node 0.13.0 → 0.15.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/README.md +102 -196
- package/index.d.ts +729 -299
- package/index.js +140 -117
- package/package.json +27 -76
- package/spikard-node.linux-x64-gnu.node +0 -0
- package/LICENSE +0 -21
- package/dist/index.d.mts +0 -475
- package/dist/index.d.ts +0 -475
- package/dist/index.js +0 -2059
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2006
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,85 +1,91 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**pnpm:**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
48
58
|
pnpm add @spikard/node
|
|
49
59
|
```
|
|
50
60
|
|
|
51
|
-
**
|
|
61
|
+
**yarn:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
yarn add @spikard/node
|
|
65
|
+
```
|
|
52
66
|
|
|
53
|
-
|
|
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 "
|
|
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
|
-
|
|
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
|
-
|
|
100
|
+
async (req: Request): Promise<User> => UserSchema.parse(req.json()),
|
|
95
101
|
);
|
|
96
102
|
|
|
97
|
-
|
|
103
|
+
if (require.main === module) {
|
|
104
|
+
app.run({ port: 8000 });
|
|
105
|
+
}
|
|
98
106
|
```
|
|
99
|
-
## Routing & Schemas
|
|
100
107
|
|
|
101
|
-
|
|
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 "
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
149
|
+
## Validation
|
|
133
150
|
|
|
134
|
-
|
|
151
|
+
```typescript
|
|
152
|
+
import { Spikard, type Request } from "spikard";
|
|
153
|
+
import { z } from "zod";
|
|
135
154
|
|
|
136
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
{
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
199
|
+
## Contributing
|
|
289
200
|
|
|
290
|
-
|
|
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
|
|
205
|
+
MIT License — see [LICENSE](https://github.com/Goldziher/spikard/blob/main/LICENSE) for details.
|