@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 +124 -350
- package/dist/index.d.mts +400 -0
- package/dist/index.d.ts +400 -0
- package/dist/index.js +1697 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1645 -0
- package/dist/index.mjs.map +1 -0
- package/index.d.ts +12 -1
- package/index.js +105 -105
- package/package.json +43 -38
- package/spikard-node.linux-x64-gnu.node +0 -0
- package/config.d.ts +0 -281
- package/graphql.d.ts +0 -287
package/README.md
CHANGED
|
@@ -1,39 +1,41 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Spikard for Node.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://spikard.dev)
|
|
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
|
+
[](../../LICENSE)
|
|
4
10
|
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
[](https://www.npmjs.com/package/spikard)
|
|
9
|
-
[](https://crates.io/crates/spikard-node)
|
|
10
|
-
[](https://docs.rs/spikard-node)
|
|
11
|
-
[](LICENSE)
|
|
12
|
-
[](https://discord.gg/pXxagNK2zN)
|
|
13
|
+
## Features
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
pnpm
|
|
26
|
+
npm install @spikard/node
|
|
27
|
+
# or
|
|
28
|
+
pnpm add @spikard/node
|
|
26
29
|
```
|
|
27
30
|
|
|
28
|
-
**Requirements:**
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
81
|
-
app.run({ port: 8000 });
|
|
82
|
-
}
|
|
77
|
+
app.run({ port: 8000 });
|
|
83
78
|
```
|
|
84
79
|
|
|
85
|
-
##
|
|
80
|
+
## Routing & Schemas
|
|
86
81
|
|
|
87
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
113
|
+
Supported HTTP methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE.
|
|
126
114
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
Access query, path params, headers, cookies, and body:
|
|
182
141
|
|
|
183
142
|
```typescript
|
|
184
|
-
get("/search")(async
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
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
|
-
##
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
266
|
-
|
|
164
|
+
**Streaming Responses:**
|
|
267
165
|
```typescript
|
|
268
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
420
|
-
const
|
|
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
|
-
##
|
|
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
|
-
|
|
480
|
-
import { runServer } from "spikard";
|
|
241
|
+
Benchmarked across 34 workloads at 100 concurrency ([methodology](../../docs/benchmarks/methodology.md)):
|
|
481
242
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
253
|
+
Spikard Node is **1.2x faster** than Kito and **2.4x faster** than Fastify.
|
|
490
254
|
|
|
491
|
-
|
|
492
|
-
- **napi-rs**
|
|
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
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
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
|