@spikard/node-native
v0.10.2
Published
High-performance HTTP framework for Node.js and Bun. Type-safe routing, validation, and testing powered by Rust core.
Maintainers
Readme
spikard-node
High-performance Node.js bindings for Spikard HTTP framework via napi-rs.
Status & Badges
Overview
High-performance TypeScript/Node.js web framework with a Rust core. Build REST APIs with type-safe routing backed by Axum and Tower-HTTP.
Installation
From source (currently):
cd packages/node
pnpm install
pnpm buildRequirements:
- Node.js 20+
- pnpm 10+
- Rust toolchain (for building from source)
Quick Start
import { Spikard, type Request } from "spikard";
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const app = new Spikard();
const getUser = async (req: Request): Promise<User> => {
const id = Number(req.params["id"] ?? 0);
return { id, name: "Alice", email: "[email protected]" };
};
const createUser = async (req: Request): Promise<User> => {
return UserSchema.parse(req.json());
};
app.addRoute(
{
method: "GET",
path: "/users/:id",
handler_name: "getUser",
is_async: true,
},
getUser,
);
app.addRoute(
{
method: "POST",
path: "/users",
handler_name: "createUser",
request_schema: UserSchema,
response_schema: UserSchema,
is_async: true,
},
createUser,
);
if (require.main === module) {
app.run({ port: 8000 });
}Route Registration
Manual Registration with addRoute
Routes are registered manually using app.addRoute(metadata, handler):
import { Spikard, type Request } from "spikard";
const app = new Spikard();
async function listUsers(_req: Request): Promise<{ users: unknown[] }> {
return { users: [] };
}
async function createUser(_req: Request): Promise<{ created: boolean }> {
return { created: true };
}
app.addRoute(
{
method: "GET",
path: "/users",
handler_name: "listUsers",
is_async: true,
},
listUsers
);
app.addRoute(
{
method: "POST",
path: "/users",
handler_name: "createUser",
is_async: true,
},
createUser
);Supported HTTP Methods
GET- Retrieve resourcesPOST- Create resourcesPUT- Replace resourcesPATCH- Update resourcesDELETE- Delete resourcesHEAD- Get headers onlyOPTIONS- Get allowed methodsTRACE- Echo the request
With Schemas
Spikard supports Zod schemas and raw JSON Schema objects.
With Zod (recommended - type inference):
import { post } from "spikard";
import { z } from "zod";
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().min(18),
});
post("/users", {
bodySchema: CreateUserSchema,
responseSchema: z.object({ id: z.number(), name: z.string() }),
})(async function createUser(req) {
const user = req.json();
return { id: 1, name: user.name };
});With raw JSON Schema:
const userSchema = {
type: "object",
properties: {
name: { type: "string" },
email: { type: "string", format: "email" },
},
required: ["name", "email"],
};
post("/users", { bodySchema: userSchema })(async function createUser(req) {
const user = req.json<{ name: string; email: string }>();
return { id: 1, ...user };
});Request Handling
Accessing Request Data
get("/search")(async function search(req) {
// Query parameters
const params = new URLSearchParams(req.queryString);
const q = params.get("q");
const limit = params.get("limit") ?? "10";
// Headers
const auth = req.headers["authorization"];
// Method and path
console.log(`${req.method} ${req.path}`);
return { query: q, limit: parseInt(limit) };
});JSON Body
post("/users")(async function createUser(req) {
const body = req.json<{ name: string; email: string }>();
return { id: 1, ...body };
});Form Data
post("/login")(async function login(req) {
const form = req.form();
return {
username: form.username,
password: form.password,
};
});Handler Wrappers
For automatic parameter extraction:
import { wrapHandler, wrapBodyHandler } from "spikard";
// Body-only wrapper
post("/users", {}, wrapBodyHandler(async (body: CreateUserRequest) => {
return { id: 1, name: body.name };
}));
// Full context wrapper
get(
"/users/:id",
{},
wrapHandler(async (params: { id: string }, query: Record<string, unknown>) => {
return { id: Number(params.id), query };
}),
);File Uploads
import { UploadFile } from "spikard";
interface UploadRequest {
file: UploadFile;
description: string;
}
post("/upload")(async function upload(req) {
const body = req.json<UploadRequest>();
const content = body.file.read();
return {
filename: body.file.filename,
size: body.file.size,
contentType: body.file.contentType,
};
});Streaming Responses
import { StreamingResponse } from "spikard";
async function* generateData() {
for (let i = 0; i < 10; i++) {
yield JSON.stringify({ count: i }) + "\n";
await new Promise((resolve) => setTimeout(resolve, 100));
}
}
get("/stream")(async function stream() {
return new StreamingResponse(generateData(), {
statusCode: 200,
headers: { "Content-Type": "application/x-ndjson" },
});
});Configuration
import { Spikard, runServer, type ServerConfig } from "spikard";
const app = new Spikard();
const config: ServerConfig = {
host: "0.0.0.0",
port: 8080,
workers: 4,
enableRequestId: true,
maxBodySize: 10 * 1024 * 1024, // 10 MB
requestTimeout: 30, // seconds
compression: {
gzip: true,
brotli: true,
quality: 9,
minSize: 1024,
},
rateLimit: {
perSecond: 100,
burst: 200,
ipBased: true,
},
jwtAuth: {
secret: "your-secret-key",
algorithm: "HS256",
},
staticFiles: [
{
directory: "./public",
routePrefix: "/static",
indexFile: true,
},
],
openapi: {
enabled: true,
title: "My API",
version: "1.0.0",
swaggerUiPath: "/docs",
redocPath: "/redoc",
},
};
runServer(app, config);Lifecycle Hooks
app.onRequest(async (request) => {
console.log(`${request.method} ${request.path}`);
return request;
});
app.preValidation(async (request) => {
// Check before validation
if (!request.headers["authorization"]) {
return {
status: 401,
body: { error: "Unauthorized" },
};
}
return request;
});
app.preHandler(async (request) => {
// After validation, before handler
return request;
});
app.onResponse(async (response) => {
response.headers["X-Frame-Options"] = "DENY";
return response;
});
app.onError(async (response) => {
console.error(`Error: ${response.status}`);
return response;
});Background Tasks
import * as background from "spikard/background";
post("/process")(async function process(req) {
const data = req.json();
background.run(() => {
// Heavy processing after response sent
processData(data);
});
return { status: "processing" };
});Testing
import { TestClient } from "spikard";
import { expect } from "vitest";
const app = {
routes: [
/* ... */
],
handlers: {
/* ... */
},
};
const client = new TestClient(app);
const response = await client.get("/users/123");
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ id: "123", name: "Alice" });WebSocket Testing
const ws = await client.websocketConnect("/ws");
await ws.sendJson({ message: "hello" });
const response = await ws.receiveJson();
expect(response.echo.message).toBe("hello");
await ws.close();SSE Testing
const response = await client.get("/events");
const sse = new SseStream(response.text());
const events = sse.eventsAsJson();
expect(events.length).toBeGreaterThan(0);Type Safety
Full TypeScript support with auto-generated types:
import {
type Request,
type Response,
type ServerConfig,
type RouteOptions,
type HandlerFunction,
} from "spikard";Parameter Types
import { Query, Path, Body, QueryDefault } from "spikard";
function handler(
id: Path<number>,
limit: Query<string | undefined>,
body: Body<UserType>
) {
// Full type inference
}Validation with Zod
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(18).optional(),
tags: z.array(z.string()).default([]),
});
post("/users", { bodySchema: UserSchema })(async function createUser(req) {
const user = req.json<z.infer<typeof UserSchema>>();
// user is fully typed and validated
return user;
});Running the Server
// Simple start
app.run({ port: 8000 });
// With full configuration
import { runServer } from "spikard";
runServer(app, {
host: "0.0.0.0",
port: 8080,
workers: 4,
});Performance
Node.js bindings use:
- napi-rs for zero-copy FFI
- ThreadsafeFunction for async JavaScript callbacks
- Dedicated Tokio runtime (doesn't block Node event loop)
- Direct type conversion without JSON serialization overhead
Examples
See /examples/node/ for more examples.
Documentation
License
MIT
