TypeScript / Node Binding¶
Node/Bun binding built with NAPI-RS. Use Spikard.addRoute metadata or method decorators (get, post, etc.) plus Zod schemas for validation.
Quickstart (metadata)¶
import { Spikard, type Request } from "@spikard/node";
import { z } from "zod";
const UserSchema = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof UserSchema>;
const app = new Spikard();
const getUser = async (req: Request): Promise<User> => {
const segments = req.path.split("/");
const id = Number(segments[segments.length - 1] ?? 0);
return { id, name: "Alice" };
};
app.addRoute(
{ method: "GET", path: "/users/:id", handler_name: "getUser", is_async: true },
getUser,
);
app.run({ port: 8000 });
Decorators (get, post, etc.) are available for metadata-only definitions, but the recommended path today is explicit addRoute with Zod schemas as above to avoid ambiguity about handler registration.
Request Handler Input¶
Handlers receive a HandlerInput object with the following properties:
interface HandlerInput {
method: string; // HTTP method (GET, POST, etc.)
path: string; // Request path
headers: Record<string, string>; // HTTP headers (lowercased keys)
cookies: Record<string, string>; // Parsed HTTP cookies
query_params: unknown; // Query string parameters
validated_params?: unknown; // Validated parameters (combined)
body: unknown; // Parsed request body (JSON)
path_params: Record<string, string>; // Path parameters (e.g., :id)
}
Handlers must return either a HandlerOutput or throw an error:
interface HandlerOutput {
status: number; // HTTP status code
headers?: Record<string, string>; // Response headers
body?: unknown; // Response body (JSON)
raw_body?: Buffer; // Pre-serialized bytes (optional)
}
Streaming Responses¶
Use StreamingResponse to send data in chunks via async iterators:
async function* streamData() {
yield Buffer.from("chunk 1\n");
yield Buffer.from("chunk 2\n");
}
app.addRoute(
{ method: "GET", path: "/stream", handler_name: "streamHandler", is_async: true },
async (req: HandlerInput) => {
const handle = createStreamingHandle(streamData(), {
status_code: 200,
headers: { "content-type": "text/plain" }
});
return handle;
}
);
Import createStreamingHandle and StreamingResponseInit from @spikard/node.
WebSocket Support¶
Register WebSocket handlers with addWebSocketRoute:
const wsHandler = {
handle_message: async (message: string): Promise<string> => {
const data = JSON.parse(message);
return JSON.stringify({ echo: data });
},
on_connect: async () => {
console.log("Client connected");
},
on_disconnect: async () => {
console.log("Client disconnected");
}
};
app.addWebSocketRoute({
path: "/ws",
handler_name: "wsHandler"
}, wsHandler);
For testing WebSocket connections, use WebSocketTestConnection:
import { WebSocketTestConnection } from "@spikard/node/testing";
const ws = await app.test_client.websocket("/ws");
await ws.send_json({ message: "hello" });
const response = await ws.receive_json();
await ws.close();
gRPC Support¶
Register gRPC services with GrpcService:
import { GrpcRequest, GrpcResponse, GrpcService } from "@spikard/node";
const userService = {
async handleRequest(request: GrpcRequest): Promise<GrpcResponse> => {
// Deserialize request using protobufjs
const req = UserService.GetUserRequest.decode(request.payload);
// Process request
const user = { id: req.id, name: "John Doe" };
// Serialize response
return {
payload: Buffer.from(UserService.User.encode(user).finish()),
metadata: { "x-user-id": String(user.id) }
};
},
};
const grpcService = new GrpcService();
grpcService.registerUnary("mypackage.UserService", "GetUser", userService);
const app = new Spikard();
app.useGrpc(grpcService);
Server Configuration¶
Pass a ServerConfig object to app.run():
interface ServerConfig {
host?: string; // Default: "127.0.0.1"
port?: number; // Default: 3000
workers?: number; // Worker threads
// Request handling
maxBodySize?: number; // Max request size (bytes)
requestTimeout?: number; // Timeout per request (seconds)
enableRequestId?: boolean; // Auto X-Request-ID header
enableHttpTrace?: boolean; // HTTP trace logging
// Shutdown
gracefulShutdown?: boolean; // Wait for in-flight requests
shutdownTimeout?: number; // Timeout for graceful shutdown (seconds)
// Middleware
compression?: {
gzip?: boolean; // Enable gzip (default: true)
brotli?: boolean; // Enable brotli (default: true)
minSize?: number; // Min size to compress (default: 1024)
quality?: number; // Compression quality (default: 6)
};
rateLimit?: {
perSecond: number; // Max requests per second
burst: number; // Burst allowance
ipBased?: boolean; // Rate limit per IP (default: true)
};
jwtAuth?: {
secret: string; // JWT secret key
algorithm?: string; // Algorithm (default: "HS256")
audience?: string[]; // Expected audiences
issuer?: string; // Expected issuer
leeway?: number; // Leeway in seconds
};
apiKeyAuth?: {
keys: string[]; // Allowed API keys
headerName?: string; // Header name (default: "X-API-Key")
};
staticFiles?: Array<{
directory: string; // Directory to serve
routePrefix: string; // Route prefix (e.g., "/public")
indexFile?: boolean; // Serve index.html (default: true)
cacheControl?: string; // Cache-Control header
}>;
openapi?: {
enabled?: boolean; // Enable OpenAPI docs
title?: string; // API title
version?: string; // API version
description?: string; // API description
swaggerUiPath?: string; // Swagger UI path (default: "/docs")
redocPath?: string; // ReDoc path (default: "/redoc")
openapiJsonPath?: string; // OpenAPI JSON path (default: "/openapi.json")
contact?: {
name?: string;
email?: string;
url?: string;
};
license?: {
name: string;
url?: string;
};
servers?: Array<{
url: string;
description?: string;
}>;
};
}
Lifecycle Hooks¶
Register hooks on the app object before calling run():
app.onRequest = [
async (request: HandlerInput) => {
// Called before each request
console.log(`${request.method} ${request.path}`);
}
];
app.preValidation = [
async (request: HandlerInput) => {
// Called before validation
}
];
app.preHandler = [
async (request: HandlerInput) => {
// Called before handler execution
}
];
app.onResponse = [
async (request: HandlerInput) => {
// Called after successful response
}
];
app.onError = [
async (request: HandlerInput) => {
// Called on error
}
];
Testing Utilities¶
Use TestClient for integration testing without running a server:
import { TestClient } from "@spikard/node";
const client = new TestClient(app);
// Make requests
const response = await client.get("/users/1");
console.log(response.status_code); // 200
console.log(response.json()); // { id: 1, name: "Alice" }
// Test uploads
const formData = new FormData();
formData.append("file", new File(["data"], "test.txt"));
const uploadResponse = await client.post("/upload", {
body: formData,
headers: { "content-type": "multipart/form-data" }
});
// WebSocket testing
const ws = await client.websocket("/ws");
await ws.send_json({ msg: "hello" });
const reply = await ws.receive_json();
await ws.close();
// SSE testing
const sse = await client.sse("/events");
const event = await sse.next_event();
await sse.close();
TestClient methods:
get(path, options?): Promise<TestResponse>post(path, options?): Promise<TestResponse>put(path, options?): Promise<TestResponse>delete(path, options?): Promise<TestResponse>patch(path, options?): Promise<TestResponse>websocket(path): Promise<WebSocketTestConnection>sse(path): Promise<SseTestConnection>
File Uploads¶
Handlers receive file uploads in body and path_params. For multipart forms, use form parsing:
app.addRoute(
{
method: "POST",
path: "/upload",
handler_name: "uploadHandler",
is_async: true,
file_params: { file: "file" } // Declare expected file param
},
async (req: HandlerInput) => {
const files = req.body.files; // Array of uploaded files
return { status: 200, body: { uploaded: files.length } };
}
);
Validation¶
Use Zod schemas for request/response validation:
const schema = z.object({
id: z.number().positive(),
name: z.string().min(1)
});
app.addRoute(
{
method: "GET",
path: "/users/:id",
handler_name: "getUser",
is_async: true,
request_schema: z.object({ id: z.string() }).parse({ id: "123" }),
response_schema: schema.parse({})
},
async (req: HandlerInput) => {
return { status: 200, body: req.validated_params };
}
);
Deployment¶
- Local:
node app.js/ts-node app.ts; setPORTviaapp.run({ port }). - Containers: build native module ahead of time (
pnpm build:native) to avoid runtime compilation.
Troubleshooting¶
- Requires Node 20+.
- Normal installs use prebuilt native binaries for supported targets.
- Rust is only required when building the native module from source.
- If params aren't parsed, double-check
pathpattern (/users/:id) and handler names. - For streaming responses, ensure iterator yields
Bufferor string chunks. - WebSocket handlers must return Promise
(JSON serialized).