@spikard/node 0.12.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 +118 -198
- package/index.d.ts +731 -290
- 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 -415
- package/dist/index.d.ts +0 -415
- package/dist/index.js +0 -1828
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1776
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,66 +1,91 @@
|
|
|
1
|
-
# Spikard
|
|
1
|
+
# Spikard
|
|
2
|
+
|
|
3
|
+
<div align="center" style="display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; margin: 20px 0;">
|
|
4
|
+
<!-- Language Bindings -->
|
|
5
|
+
<a href="https://crates.io/crates/spikard">
|
|
6
|
+
<img src="https://img.shields.io/crates/v/spikard?label=Rust&color=007ec6" alt="Rust">
|
|
7
|
+
</a>
|
|
8
|
+
<a href="https://pypi.org/project/spikard/">
|
|
9
|
+
<img src="https://img.shields.io/pypi/v/spikard?label=Python&color=007ec6" alt="Python">
|
|
10
|
+
</a>
|
|
11
|
+
<a href="https://www.npmjs.com/package/@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">
|
|
16
|
+
</a>
|
|
17
|
+
<a href="https://rubygems.org/gems/spikard">
|
|
18
|
+
<img src="https://img.shields.io/gem/v/spikard?label=Ruby&color=007ec6" alt="Ruby">
|
|
19
|
+
</a>
|
|
20
|
+
<a href="https://packagist.org/packages/spikard/spikard">
|
|
21
|
+
<img src="https://img.shields.io/packagist/v/spikard/spikard?label=PHP&color=007ec6" alt="PHP">
|
|
22
|
+
</a>
|
|
23
|
+
<a href="https://hex.pm/packages/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#">
|
|
34
|
+
</a>
|
|
35
|
+
|
|
36
|
+
<!-- Project Info -->
|
|
37
|
+
<a href="https://github.com/Goldziher/spikard/blob/main/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">
|
|
42
|
+
</a>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
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.
|
|
2
46
|
|
|
3
|
-
|
|
4
|
-
[](https://crates.io/crates/spikard)
|
|
5
|
-
[](https://pypi.org/project/spikard/)
|
|
6
|
-
[](https://www.npmjs.com/package/@spikard/node)
|
|
7
|
-
[](https://rubygems.org/gems/spikard)
|
|
8
|
-
[](https://packagist.org/packages/spikard/spikard)
|
|
9
|
-
[](https://hex.pm/packages/spikard)
|
|
10
|
-
[](../../LICENSE)
|
|
47
|
+
## Installation
|
|
11
48
|
|
|
12
|
-
|
|
49
|
+
**npm:**
|
|
13
50
|
|
|
14
|
-
|
|
51
|
+
```bash
|
|
52
|
+
npm install @spikard/node
|
|
53
|
+
```
|
|
15
54
|
|
|
16
|
-
|
|
17
|
-
- **Full TypeScript Support**: Auto-generated types from napi-rs FFI bindings
|
|
18
|
-
- **Zero-Copy JSON**: Direct conversion without serialization overhead
|
|
19
|
-
- **Tower-HTTP Middleware**: Compression, rate limiting, timeouts, auth, CORS, request IDs
|
|
20
|
-
- **Schema Validation**: Zod integration for request/response validation
|
|
21
|
-
- **Lifecycle Hooks**: onRequest, preValidation, preHandler, onResponse, onError
|
|
22
|
-
- **Testing**: TestClient for HTTP, WebSocket, and SSE assertions
|
|
23
|
-
|
|
24
|
-
## Installation
|
|
55
|
+
**pnpm:**
|
|
25
56
|
|
|
26
57
|
```bash
|
|
27
|
-
npm install @spikard/node
|
|
28
|
-
# or
|
|
29
58
|
pnpm add @spikard/node
|
|
30
59
|
```
|
|
31
60
|
|
|
32
|
-
**
|
|
61
|
+
**yarn:**
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
yarn add @spikard/node
|
|
65
|
+
```
|
|
33
66
|
|
|
34
|
-
|
|
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)
|
|
35
71
|
|
|
36
72
|
## Quick Start
|
|
37
73
|
|
|
38
74
|
```typescript
|
|
39
|
-
import { Spikard, type Request } from "
|
|
75
|
+
import { Spikard, type Request } from "spikard";
|
|
40
76
|
import { z } from "zod";
|
|
41
77
|
|
|
42
|
-
const UserSchema = z.object({
|
|
43
|
-
id: z.number(),
|
|
44
|
-
name: z.string(),
|
|
45
|
-
email: z.string().email(),
|
|
46
|
-
});
|
|
47
|
-
|
|
78
|
+
const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
48
79
|
type User = z.infer<typeof UserSchema>;
|
|
49
80
|
|
|
50
81
|
const app = new Spikard();
|
|
51
82
|
|
|
52
|
-
const getUser = async (req: Request): Promise<User> => {
|
|
53
|
-
const id = Number(req.params["id"] ?? 0);
|
|
54
|
-
return { id, name: "Alice", email: "alice@example.com" };
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const createUser = async (req: Request): Promise<User> => {
|
|
58
|
-
return UserSchema.parse(req.json());
|
|
59
|
-
};
|
|
60
|
-
|
|
61
83
|
app.addRoute(
|
|
62
84
|
{ method: "GET", path: "/users/:id", handler_name: "getUser", is_async: true },
|
|
63
|
-
|
|
85
|
+
async (req: Request): Promise<User> => {
|
|
86
|
+
const id = Number(req.params["id"] ?? 0);
|
|
87
|
+
return { id, name: "Alice" };
|
|
88
|
+
},
|
|
64
89
|
);
|
|
65
90
|
|
|
66
91
|
app.addRoute(
|
|
@@ -72,32 +97,42 @@ app.addRoute(
|
|
|
72
97
|
response_schema: UserSchema,
|
|
73
98
|
is_async: true,
|
|
74
99
|
},
|
|
75
|
-
|
|
100
|
+
async (req: Request): Promise<User> => UserSchema.parse(req.json()),
|
|
76
101
|
);
|
|
77
102
|
|
|
78
|
-
|
|
103
|
+
if (require.main === module) {
|
|
104
|
+
app.run({ port: 8000 });
|
|
105
|
+
}
|
|
79
106
|
```
|
|
80
107
|
|
|
81
|
-
##
|
|
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
|
|
82
116
|
|
|
83
|
-
|
|
117
|
+
## Routing
|
|
84
118
|
|
|
85
119
|
```typescript
|
|
86
|
-
import { Spikard, type Request } from "
|
|
120
|
+
import { Spikard, type Request } from "spikard";
|
|
87
121
|
import { z } from "zod";
|
|
88
122
|
|
|
123
|
+
const UserSchema = z.object({ id: z.number(), name: z.string() });
|
|
124
|
+
type User = z.infer<typeof UserSchema>;
|
|
125
|
+
|
|
89
126
|
const app = new Spikard();
|
|
90
127
|
|
|
91
|
-
const
|
|
92
|
-
name: z.string().min(1),
|
|
93
|
-
email: z.string().email(),
|
|
94
|
-
});
|
|
128
|
+
const health = async (): Promise<{ status: string }> => ({ status: "ok" });
|
|
95
129
|
|
|
96
|
-
const createUser = async (req: Request) => {
|
|
97
|
-
|
|
98
|
-
return { id: 1, ...user };
|
|
130
|
+
const createUser = async (req: Request): Promise<User> => {
|
|
131
|
+
return UserSchema.parse(req.json());
|
|
99
132
|
};
|
|
100
133
|
|
|
134
|
+
app.addRoute({ method: "GET", path: "/health", handler_name: "health", is_async: true }, health);
|
|
135
|
+
|
|
101
136
|
app.addRoute(
|
|
102
137
|
{
|
|
103
138
|
method: "POST",
|
|
@@ -111,175 +146,60 @@ app.addRoute(
|
|
|
111
146
|
);
|
|
112
147
|
```
|
|
113
148
|
|
|
114
|
-
|
|
149
|
+
## Validation
|
|
115
150
|
|
|
116
|
-
|
|
151
|
+
```typescript
|
|
152
|
+
import { Spikard, type Request } from "spikard";
|
|
153
|
+
import { z } from "zod";
|
|
117
154
|
|
|
118
|
-
|
|
155
|
+
const PaymentSchema = z.object({
|
|
156
|
+
id: z.string().uuid(),
|
|
157
|
+
amount: z.number().positive(),
|
|
158
|
+
});
|
|
159
|
+
type Payment = z.infer<typeof PaymentSchema>;
|
|
119
160
|
|
|
120
|
-
```typescript
|
|
121
161
|
const app = new Spikard();
|
|
122
162
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
async ({ config }) => ({ url: config.dbUrl, driver: "pool" }),
|
|
127
|
-
{ dependsOn: ["config"], singleton: true },
|
|
128
|
-
);
|
|
163
|
+
const createPayment = async (req: Request): Promise<Payment> => {
|
|
164
|
+
return PaymentSchema.parse(req.json());
|
|
165
|
+
};
|
|
129
166
|
|
|
130
167
|
app.addRoute(
|
|
131
|
-
{
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
168
|
+
{
|
|
169
|
+
method: "POST",
|
|
170
|
+
path: "/payments",
|
|
171
|
+
handler_name: "createPayment",
|
|
172
|
+
request_schema: PaymentSchema,
|
|
173
|
+
response_schema: PaymentSchema,
|
|
174
|
+
is_async: true,
|
|
135
175
|
},
|
|
176
|
+
createPayment,
|
|
136
177
|
);
|
|
137
178
|
```
|
|
138
179
|
|
|
139
|
-
##
|
|
140
|
-
|
|
141
|
-
Access query, path params, headers, cookies, and body:
|
|
142
|
-
|
|
143
|
-
```typescript
|
|
144
|
-
get("/search")(async (req) => {
|
|
145
|
-
const q = req.query.q;
|
|
146
|
-
const id = req.params.id;
|
|
147
|
-
const auth = req.headers.authorization;
|
|
148
|
-
const session = req.cookies.session_id;
|
|
149
|
-
const body = req.json<{ name: string }>();
|
|
150
|
-
const form = req.form();
|
|
151
|
-
return { query: q, id };
|
|
152
|
-
});
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Advanced Features
|
|
180
|
+
## Middleware
|
|
156
181
|
|
|
157
|
-
**File Uploads:**
|
|
158
182
|
```typescript
|
|
159
|
-
|
|
160
|
-
const body = req.json<{ file: UploadFile }>();
|
|
161
|
-
return { filename: body.file.filename, size: body.file.size };
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
**Streaming Responses:**
|
|
166
|
-
```typescript
|
|
167
|
-
get("/stream")(async function* () {
|
|
168
|
-
for (let i = 0; i < 10; i++) {
|
|
169
|
-
yield JSON.stringify({ count: i }) + "\n";
|
|
170
|
-
await new Promise(r => setTimeout(r, 100));
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
## Configuration
|
|
176
|
-
|
|
177
|
-
Configure middleware, compression, rate limiting, and authentication:
|
|
183
|
+
import { Spikard, type Request } from "spikard";
|
|
178
184
|
|
|
179
|
-
|
|
180
|
-
const config: ServerConfig = {
|
|
181
|
-
port: 8080,
|
|
182
|
-
workers: 4,
|
|
183
|
-
maxBodySize: 10 * 1024 * 1024,
|
|
184
|
-
requestTimeout: 30,
|
|
185
|
-
compression: { gzip: true, brotli: true, minSize: 1024 },
|
|
186
|
-
rateLimit: { perSecond: 100, burst: 200 },
|
|
187
|
-
jwtAuth: { secret: "key", algorithm: "HS256" },
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
app.run(config);
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
See [ServerConfig](../../docs/adr/0002-runtime-and-middleware.md) for all options.
|
|
194
|
-
|
|
195
|
-
## Lifecycle Hooks
|
|
196
|
-
|
|
197
|
-
Execute code at key request/response stages:
|
|
185
|
+
const app = new Spikard();
|
|
198
186
|
|
|
199
|
-
|
|
200
|
-
app.onRequest(async (request) => {
|
|
187
|
+
app.onRequest(async (request: Request): Promise<Request> => {
|
|
201
188
|
console.log(`${request.method} ${request.path}`);
|
|
202
189
|
return request;
|
|
203
190
|
});
|
|
204
|
-
|
|
205
|
-
app.preValidation(async (request) => {
|
|
206
|
-
if (!request.headers["authorization"]) {
|
|
207
|
-
return { status: 401, body: { error: "Unauthorized" } };
|
|
208
|
-
}
|
|
209
|
-
return request;
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
app.onResponse(async (response) => {
|
|
213
|
-
response.headers["X-Frame-Options"] = "DENY";
|
|
214
|
-
return response;
|
|
215
|
-
});
|
|
216
191
|
```
|
|
217
192
|
|
|
218
|
-
## Testing
|
|
219
|
-
|
|
220
|
-
Use TestClient for HTTP, WebSocket, and SSE testing:
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
import { TestClient } from "@spikard/node";
|
|
224
|
-
import { expect } from "vitest";
|
|
225
|
-
|
|
226
|
-
const client = new TestClient(app);
|
|
227
|
-
|
|
228
|
-
// HTTP testing
|
|
229
|
-
const response = await client.get("/users/123");
|
|
230
|
-
expect(response.statusCode).toBe(200);
|
|
231
|
-
|
|
232
|
-
// WebSocket testing
|
|
233
|
-
const ws = await client.websocketConnect("/ws");
|
|
234
|
-
await ws.sendJson({ message: "hello" });
|
|
235
|
-
|
|
236
|
-
// SSE testing
|
|
237
|
-
const sse = await client.get("/events");
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
## Performance
|
|
241
|
-
|
|
242
|
-
Benchmarked across 34 workloads at 100 concurrency ([methodology](../../docs/benchmarks/methodology.md)):
|
|
243
|
-
|
|
244
|
-
| Framework | Avg RPS | P50 (ms) | P99 (ms) |
|
|
245
|
-
|-----------|--------:|----------:|----------:|
|
|
246
|
-
| **spikard (Bun)** | 49,460 | 2.18 | 4.21 |
|
|
247
|
-
| **spikard (Node)** | 46,160 | 2.18 | 3.35 |
|
|
248
|
-
| elysia | 44,326 | 2.41 | 4.68 |
|
|
249
|
-
| kito | 36,958 | 4.94 | 12.86 |
|
|
250
|
-
| fastify | 19,167 | 6.74 | 14.76 |
|
|
251
|
-
| morojs | 14,196 | 6.44 | 12.61 |
|
|
252
|
-
| hono | 10,928 | 10.91 | 18.62 |
|
|
253
|
-
|
|
254
|
-
Spikard Node is **1.2x faster** than Kito and **2.4x faster** than Fastify.
|
|
255
|
-
|
|
256
|
-
Key optimizations:
|
|
257
|
-
- **napi-rs** zero-copy FFI bindings
|
|
258
|
-
- **Dedicated Tokio runtime** without blocking Node event loop
|
|
259
|
-
- **Zero-copy JSON** conversion (30-40% faster than JSON.parse)
|
|
260
|
-
- **ThreadsafeFunction** for async JavaScript callbacks
|
|
261
|
-
|
|
262
|
-
## Examples
|
|
263
|
-
|
|
264
|
-
See [examples/](../../examples/) for runnable projects. Code generation is supported for OpenAPI, GraphQL, AsyncAPI, and JSON-RPC specifications.
|
|
265
|
-
|
|
266
193
|
## Documentation
|
|
267
194
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
198
|
|
|
272
|
-
|
|
199
|
+
## Contributing
|
|
273
200
|
|
|
274
|
-
|
|
275
|
-
|----------|---------|--------|
|
|
276
|
-
| **Node.js** | [@spikard/node](https://www.npmjs.com/package/@spikard/node) | Stable |
|
|
277
|
-
| **Python** | [spikard](https://pypi.org/project/spikard/) | Stable |
|
|
278
|
-
| **Rust** | [spikard](https://crates.io/crates/spikard) | Stable |
|
|
279
|
-
| **Ruby** | [spikard](https://rubygems.org/gems/spikard) | Stable |
|
|
280
|
-
| **PHP** | [spikard/spikard](https://packagist.org/packages/spikard/spikard) | Stable |
|
|
281
|
-
| **Elixir** | [spikard](https://hex.pm/packages/spikard) | Stable |
|
|
201
|
+
Contributions are welcome. See [CONTRIBUTING.md](https://github.com/Goldziher/spikard/blob/main/CONTRIBUTING.md).
|
|
282
202
|
|
|
283
203
|
## License
|
|
284
204
|
|
|
285
|
-
MIT
|
|
205
|
+
MIT License — see [LICENSE](https://github.com/Goldziher/spikard/blob/main/LICENSE) for details.
|