An open API service indexing awesome lists of open source software.

https://github.com/matthewthecoder1218/princejs

The smallest backend framework and among the top three in the world.
https://github.com/matthewthecoder1218/princejs

api backend bun database fast framework jose jwt lightweight middleware princejs rest scheduler server sse typescript websocket zod

Last synced: 16 days ago
JSON representation

The smallest backend framework and among the top three in the world.

Awesome Lists containing this project

README

          

# ๐Ÿ‘‘ PrinceJS

**Ultra-clean, modern & minimal Bun web framework.**
Built by a 13-year-old Nigerian developer. Among the top three in performance.

[![npm version](https://img.shields.io/npm/v/princejs?style=flat-square)](https://www.npmjs.com/package/princejs)
[![GitHub stars](https://img.shields.io/github/stars/MatthewTheCoder1218/princejs?style=flat-square)](https://github.com/MatthewTheCoder1218/princejs)
[![npm downloads](https://img.shields.io/npm/dt/princejs?style=flat-square)](https://www.npmjs.com/package/princejs)
[![license](https://img.shields.io/github/license/MatthewTheCoder1218/princejs?style=flat-square)](https://github.com/MatthewTheCoder1218/princejs/blob/main/LICENSE)

[**Website**](https://princejs.vercel.app) ยท [**npm**](https://www.npmjs.com/package/princejs) ยท [**GitHub**](https://github.com/MatthewTheCoder1218/princejs) ยท [**Twitter**](https://twitter.com/princejs_bun)

---

## โšก Performance

Benchmarked with `oha -c 100 -z 30s` on Windows 10:

| Framework | Avg Req/s | Peak Req/s |
|-----------|----------:|-----------:|
| Elysia | 27,606 | 27,834 |
| **PrinceJS** | **17,985** | **18,507** |
| Hono | 17,914 | 18,826 |
| Fastify | 15,519 | 16,434 |
| Express | 13,138 | 13,458 |

> PrinceJS is **2.3ร— faster than Express**, matches Hono head-to-head, and sits at approximately 5kB gzipped โ€” loads in approximately 100ms on a slow 3G connection.

---

## ๐Ÿš€ Quick Start

```bash
bun add princejs
# or
npm install princejs
```

```ts
import { prince } from "princejs";
import { cors, logger } from "princejs/middleware";

const app = prince();

app.use(cors());
app.use(logger());

app.get("/", () => ({ message: "Hello PrinceJS!" }));
app.get("/users/:id", (req) => ({ id: req.params?.id }));

app.listen(3000);
```

---

## ๐Ÿงฐ Features

| Feature | Import |
|---------|--------|
| Routing, Route Grouping, WebSockets, OpenAPI, Plugins, Lifecycle Hooks, Cookies, IP | `princejs` |
| CORS, Logger, JWT, JWKS, Auth, Rate Limit, Validate, Compress, Session, API Key, Secure Headers, Timeout, Request ID, IP Restriction, Static Files, Trim Trailing Slash, Middleware Combinators (`every`, `some`, `except`), `guard()` | `princejs/middleware` |
| File Uploads, SSE, Streaming, In-memory Cache | `princejs/helpers` |
| Cron Scheduler | `princejs/scheduler` |
| JSX / SSR | `princejs/jsx` |
| SQLite Database | `princejs/db` |
| End-to-End Type Safety | `princejs/client` |
| Vercel Edge adapter | `princejs/vercel` |
| Cloudflare Workers adapter | `princejs/cloudflare` |
| Deno Deploy adapter | `princejs/deno` |
| Node.js / Express adapter | `princejs/node` |

---

## ๐Ÿช Cookies & ๐ŸŒ IP Detection

### Reading Cookies

Cookies are automatically parsed and available on every request:

```ts
import { prince } from "princejs";

const app = prince();

app.get("/profile", (req) => ({
sessionId: req.cookies?.sessionId,
theme: req.cookies?.theme,
allCookies: req.cookies, // Record
}));
```

### Setting Cookies

Use the response builder for full cookie control:

```ts
app.get("/login", (req) =>
app.response()
.status(200)
.json({ ok: true })
.cookie("sessionId", "abc123", {
maxAge: 3600, // 1 hour
path: "/",
httpOnly: true, // not accessible from JS
secure: true, // HTTPS only
sameSite: "Strict", // CSRF protection
})
);

// Chain multiple cookies
app.response()
.json({ ok: true })
.cookie("session", "xyz")
.cookie("theme", "dark")
.cookie("lang", "en");
```

### Client IP Detection

```ts
app.get("/api/data", (req) => ({
clientIp: req.ip,
data: [],
}));
```

**Supported headers** (in priority order):
- `X-Forwarded-For` โ€” load balancers, proxies (first IP in list)
- `X-Real-IP` โ€” Nginx, Apache reverse proxy
- `CF-Connecting-IP` โ€” Cloudflare
- `X-Client-IP` โ€” other proxy services
- Fallback โ€” `127.0.0.1`

```ts
// IP-based rate limiting
app.use((req, next) => {
const count = ipTracker.getCount(req.ip) || 0;
if (count > 100) return new Response("Too many requests", { status: 429 });
ipTracker.increment(req.ip);
return next();
});

// IP allowlist
app.post("/admin", (req) => {
if (!ALLOWED_IPS.includes(req.ip!)) {
return new Response("Forbidden", { status: 403 });
}
return { authorized: true };
});
```

---

## ๐Ÿ—‚๏ธ Route Grouping

Group routes under a shared prefix with optional shared middleware. Zero overhead at request time โ€” purely a registration convenience.

```ts
import { prince } from "princejs";

const app = prince();

// Basic grouping
app.group("/api", (r) => {
r.get("/users", () => ({ users: [] }));
r.post("/users", (req) => ({ created: req.parsedBody }));
r.get("/users/:id", (req) => ({ id: req.params?.id }));
});
// โ†’ GET /api/users
// โ†’ POST /api/users
// โ†’ GET /api/users/:id

// With shared middleware โ€” applies to every route in the group
import { auth } from "princejs/middleware";

app.group("/admin", auth(), (r) => {
r.get("/stats", () => ({ stats: {} }));
r.delete("/users/:id", (req) => ({ deleted: req.params?.id }));
});

// Chainable
app
.group("/v1", (r) => { r.get("/ping", () => ({ v: 1 })); })
.group("/v2", (r) => { r.get("/ping", () => ({ v: 2 })); });

app.listen(3000);
```

---

## ๐Ÿ›ก๏ธ Secure Headers

One call sets all the security headers your production app needs:

```ts
import { secureHeaders } from "princejs/middleware";

app.use(secureHeaders());
// Sets: X-Frame-Options, X-Content-Type-Options, X-XSS-Protection,
// Strict-Transport-Security, Referrer-Policy

// Custom options
app.use(secureHeaders({
xFrameOptions: "DENY",
contentSecurityPolicy: "default-src 'self'",
permissionsPolicy: "camera=(), microphone=()",
strictTransportSecurity: "max-age=63072000; includeSubDomains; preload",
}));
```

---

## โฑ๏ธ Request Timeout

Kill hanging requests before they pile up:

```ts
import { timeout } from "princejs/middleware";

app.use(timeout(5000)); // 5 second global timeout โ†’ 408
app.use(timeout(3000, "Slow!")); // custom message

// Per-route timeout
app.get("/heavy", timeout(10000), (req) => heavyOperation());
```

---

## ๐Ÿท๏ธ Request ID

Attach a unique ID to every request for distributed tracing and log correlation:

```ts
import { requestId } from "princejs/middleware";

app.use(requestId());
// โ†’ sets req.id and X-Request-ID response header

// Custom header name
app.use(requestId({ header: "X-Trace-ID" }));

// Custom generator
app.use(requestId({ generator: () => `req-${Date.now()}` }));

app.get("/", (req) => ({ requestId: req.id }));
```

---

## ๐Ÿšซ IP Restriction

Allow or block specific IPs:

```ts
import { ipRestriction } from "princejs/middleware";

// Only allow these IPs
app.use(ipRestriction({ allowList: ["192.168.1.1", "10.0.0.1"] }));

// Block these IPs
app.use(ipRestriction({ denyList: ["1.2.3.4"] }));
```

---

## โœ‚๏ธ Trim Trailing Slash

Automatically redirect `/users/` โ†’ `/users` so you never get mysterious 404s from a stray trailing slash:

```ts
import { trimTrailingSlash } from "princejs/middleware";

app.use(trimTrailingSlash()); // 301 by default
app.use(trimTrailingSlash(302)); // or 302 temporary redirect
```

Root `/` is never redirected. Query strings are preserved โ€” `/search/?q=bun` โ†’ `/search?q=bun`.

---

## ๐Ÿ”€ Middleware Combinators

Compose complex auth rules in a single readable line.

### `every()` โ€” all must pass

```ts
import { every } from "princejs/middleware";

const isAdmin = async (req, next) => {
if (req.user?.role !== "admin")
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
return next();
};

app.get("/admin", every(auth(), isAdmin), () => ({ ok: true }));
// short-circuits on first rejection โ€” isAdmin never runs if auth() fails
```

### `some()` โ€” either must pass

```ts
import { some } from "princejs/middleware";

// Accept a JWT token OR an API key โ€” whichever the client sends
app.get("/resource", some(auth(), apiKey({ keys: ["key_123"] })), () => ({ ok: true }));
```

### `except()` โ€” skip middleware for certain paths

```ts
import { except } from "princejs/middleware";

// Apply auth everywhere except /health and /
app.use(except(["/health", "/"], auth()));

app.get("/health", () => ({ ok: true })); // no auth
app.get("/private", (req) => ({ user: req.user })); // auth required
```

---

## ๐Ÿ›ก๏ธ guard()

Apply a validation schema to every route in a group at once โ€” no need to repeat `validate()` on each handler:

```ts
import { guard } from "princejs/middleware";
import { z } from "zod";

app.group("/users", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
r.post("/", (req) => ({ created: req.parsedBody.name })); // auto-validated
r.put("/:id", (req) => ({ updated: req.parsedBody.name })); // auto-validated
});
// Bad body โ†’ 400 { error: "Validation failed", details: [...] }
```

Also works as standalone route middleware:

```ts
app.post(
"/items",
guard({ body: z.object({ name: z.string(), price: z.number() }) }),
(req) => ({ created: req.parsedBody })
);
```

---

## ๐Ÿ“ Static Files

Serve a directory of static files. Falls through to your routes if the file doesn't exist:

```ts
import { serveStatic } from "princejs/middleware";

app.use(serveStatic("./public"));
// โ†’ GET /logo.png serves ./public/logo.png
// โ†’ GET / serves ./public/index.html
// โ†’ GET /api/users falls through to your route handler
```

---

## ๐ŸŒŠ Streaming

Stream chunked responses for AI/LLM output, large payloads, or anything that generates data over time:

```ts
import { stream } from "princejs/helpers";

// Async generator โ€” cleanest for AI token streaming
app.get("/ai", stream(async function*(req) {
yield "Hello ";
await delay(100);
yield "from ";
yield "PrinceJS!";
}));

// Async callback
app.get("/data", stream(async (req) => {
req.streamSend("chunk 1");
await fetchMoreData();
req.streamSend("chunk 2");
}));

// Custom content type for binary or JSON streams
app.get("/events", stream(async function*(req) {
for (const item of items) {
yield JSON.stringify(item) + "\n";
}
}, { contentType: "application/x-ndjson" }));
```

---

## ๐Ÿ”‘ JWKS / Third-Party Auth

Verify JWTs from Auth0, Clerk, Supabase, or any JWKS endpoint โ€” no symmetric key needed:

```ts
import { jwks } from "princejs/middleware";

// Auth0
app.use(jwks("https://your-domain.auth0.com/.well-known/jwks.json"));

// Clerk
app.use(jwks("https://your-clerk-domain.clerk.accounts.dev/.well-known/jwks.json"));

// Supabase
app.use(jwks("https://your-project.supabase.co/auth/v1/.well-known/jwks.json"));

// req.user is set after verification, same as jwt()
app.get("/protected", auth(), (req) => ({ user: req.user }));
```

---

## ๐Ÿ“– OpenAPI + Scalar Docs โœจ

Auto-generate an OpenAPI 3.0 spec and serve a beautiful [Scalar](https://scalar.com) UI โ€” all from a single `app.openapi()` call.

```ts
import { prince } from "princejs";
import { z } from "zod";

const app = prince();

const api = app.openapi({ title: "My API", version: "1.0.0" }, "/docs", { theme: "moon" });

api.route("GET", "/users/:id", {
summary: "Get user by ID",
tags: ["users"],
schema: {
response: z.object({ id: z.string(), name: z.string() }),
},
}, (req) => ({ id: req.params!.id, name: "Alice" }));

api.route("POST", "/users", {
summary: "Create user",
tags: ["users"],
schema: {
body: z.object({ name: z.string().min(2), email: z.string().email() }),
response: z.object({ id: z.string(), name: z.string(), email: z.string() }),
},
}, (req) => ({ id: crypto.randomUUID(), ...req.parsedBody }));

app.listen(3000);
// โ†’ GET /docs Scalar UI
// โ†’ GET /docs.json Raw OpenAPI JSON
```

`api.route()` does three things at once:

- โœ… Registers the route on PrinceJS
- โœ… Auto-wires body validation โ€” no separate middleware needed
- โœ… Writes the full OpenAPI spec entry

| `schema` key | Runtime effect | Scalar docs |
|---|---|---|
| `body` | โœ… Validates & rejects bad requests | โœ… requestBody model |
| `query` | โ€” | โœ… Typed query params |
| `response` | โ€” | โœ… 200 response model |

> Routes on `app.get()` / `app.post()` stay private โ€” they never appear in the docs.

**Themes:** `default` ยท `moon` ยท `purple` ยท `solarized` ยท `bluePlanet` ยท `deepSpace` ยท `saturn` ยท `kepler` ยท `mars`

---

## ๐Ÿ”Œ Plugin System

```ts
import { prince, type PrincePlugin } from "princejs";

const usersPlugin: PrincePlugin<{ prefix?: string }> = (app, opts) => {
const base = opts?.prefix ?? "";

app.use((req, next) => {
(req as any).fromPlugin = true;
return next();
});

app.get(`${base}/users`, (req) => ({
ok: true,
fromPlugin: (req as any).fromPlugin,
}));
};

const app = prince();
app.plugin(usersPlugin, { prefix: "/api" });
app.listen(3000);
```

---

## ๐ŸŽฃ Lifecycle Hooks

```ts
import { prince } from "princejs";

const app = prince();

app.onRequest((req) => {
(req as any).startTime = Date.now();
});

app.onBeforeHandle((req, path, method) => {
console.log(`๐Ÿ” ${method} ${path}`);
});

app.onAfterHandle((req, res, path, method) => {
const ms = Date.now() - (req as any).startTime;
console.log(`โœ… ${method} ${path} ${res.status} (${ms}ms)`);
});

app.onError((err, req, path, method) => {
console.error(`โŒ ${method} ${path}:`, err.message);
});

app.get("/users", () => ({ users: [] }));
app.listen(3000);
```

**Execution order:**
1. `onRequest` โ€” runs before routing, good for setup
2. `onBeforeHandle` โ€” just before the handler
3. Handler executes
4. `onAfterHandle` โ€” after success (skipped on error)
5. `onError` โ€” only when handler throws

---

## ๐Ÿ”’ End-to-End Type Safety

```ts
import { createClient, type PrinceApiContract } from "princejs/client";

type ApiContract = {
"GET /users/:id": {
params: { id: string };
response: { id: string; name: string };
};
"POST /users": {
body: { name: string };
response: { id: string; ok: boolean };
};
};

const client = createClient("http://localhost:3000");

const user = await client.get("/users/:id", { params: { id: "42" } });
console.log(user.name); // typed as string โœ…

const created = await client.post("/users", { body: { name: "Alice" } });
console.log(created.id); // typed as string โœ…
```

---

## ๐ŸŒ Deploy Adapters

**Vercel Edge** โ€” `api/[[...route]].ts`
```ts
import { toVercel } from "princejs/vercel";
export default toVercel(app);
```

**Cloudflare Workers** โ€” `src/index.ts`
```ts
import { toWorkers } from "princejs/cloudflare";
export default toWorkers(app);
```

**Deno Deploy** โ€” `main.ts`
```ts
import { toDeno } from "princejs/deno";
Deno.serve(toDeno(app));
```

**Node.js** โ€” `server.ts`
```ts
import { createServer } from "http";
import { toNode, toExpress } from "princejs/node";
import express from "express";

const app = prince();
app.get("/", () => ({ message: "Hello!" }));

// Native Node http
createServer(toNode(app)).listen(3000);

// Or drop into Express
const expressApp = express();
expressApp.all("*", toExpress(app));
expressApp.listen(3000);
```

---

## ๐ŸŽฏ Full Example

```ts
import { prince } from "princejs";
import {
cors,
logger,
rateLimit,
auth,
apiKey,
jwt,
signJWT,
session,
compress,
validate,
secureHeaders,
timeout,
requestId,
trimTrailingSlash,
every,
some,
except,
guard,
} from "princejs/middleware";
import { cache, upload, sse, stream } from "princejs/helpers";
import { cron } from "princejs/scheduler";
import { Html, Head, Body, H1, P, render } from "princejs/jsx";
import { db } from "princejs/db";
import { z } from "zod";

const SECRET = new TextEncoder().encode("your-secret");
const app = prince();

// โ”€โ”€ Lifecycle hooks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.onRequest((req) => { (req as any).t = Date.now(); });
app.onAfterHandle((req, res, path, method) => {
console.log(`โœ… ${method} ${path} ${res.status} (${Date.now() - (req as any).t}ms)`);
});
app.onError((err, req, path, method) => {
console.error(`โŒ ${method} ${path}:`, err.message);
});

// โ”€โ”€ Global middleware โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.use(secureHeaders());
app.use(requestId());
app.use(trimTrailingSlash());
app.use(timeout(10000));
app.use(cors());
app.use(logger());
app.use(rateLimit(100, 60));
app.use(jwt(SECRET));
app.use(session({ secret: "session-secret" }));
app.use(compress());

// โ”€โ”€ JSX SSR โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const Page = () => Html(Head("Home"), Body(H1("Hello World"), P("Welcome!")));
app.get("/", () => render(Page()));

// โ”€โ”€ Cookies & IP โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.post("/login", (req) =>
app.response()
.json({ ok: true, ip: req.ip })
.cookie("sessionId", "user_123", {
httpOnly: true, secure: true, sameSite: "Strict", maxAge: 86400,
})
);
app.get("/profile", (req) => ({
sessionId: req.cookies?.sessionId,
clientIp: req.ip,
}));

// โ”€โ”€ Database โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const users = db.sqlite("./app.sqlite", `
CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, name TEXT NOT NULL)
`);
app.get("/users", () => users.query("SELECT * FROM users"));

// โ”€โ”€ WebSockets โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.ws("/chat", {
open: (ws) => ws.send("Welcome!"),
message: (ws, msg) => ws.send(`Echo: ${msg}`),
close: (ws) => console.log("disconnected"),
});

// โ”€โ”€ Auth & API keys โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.get("/protected", auth(), (req) => ({ user: req.user }));
app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
app.get("/admin", every(auth(), async (req, next) => {
if (req.user?.role !== "admin")
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
return next();
}), () => ({ admin: true }));

// โ”€โ”€ Validated route group โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.group("/items", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
r.post("/", (req) => ({ created: req.parsedBody.name }));
});

// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.get("/cached", cache(60)(() => ({ time: Date.now() })));
app.post("/upload", upload());
app.get("/events", sse(), (req) => {
let i = 0;
const id = setInterval(() => {
req.sseSend({ count: i++ });
if (i >= 10) clearInterval(id);
}, 1000);
});

// โ”€โ”€ Validation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
app.post(
"/items",
validate(z.object({ name: z.string().min(1), price: z.number().positive() })),
(req) => ({ created: req.parsedBody })
);

// โ”€โ”€ Cron โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
cron("* * * * *", () => console.log("๐Ÿ’“ heartbeat"));

// โ”€โ”€ OpenAPI + Scalar โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const api = app.openapi({ title: "PrinceJS App", version: "1.0.0" }, "/docs");

api.route("GET", "/items", {
summary: "List items",
tags: ["items"],
schema: {
query: z.object({ q: z.string().optional() }),
response: z.array(z.object({ id: z.string(), name: z.string() })),
},
}, () => [{ id: "1", name: "Widget" }]);

api.route("POST", "/items", {
summary: "Create item",
tags: ["items"],
schema: {
body: z.object({ name: z.string().min(1), price: z.number().positive() }),
response: z.object({ id: z.string(), name: z.string() }),
},
}, (req) => ({ id: crypto.randomUUID(), name: req.parsedBody.name }));

app.listen(3000);
```

---

## ๐Ÿ“ฆ Installation

```bash
bun add princejs
# or
npm install princejs
```

---

## ๐Ÿค Contributing

```bash
git clone https://github.com/MatthewTheCoder1218/princejs
cd princejs
bun install
bun test
```

---

## ๐Ÿ”— Links

- ๐ŸŒ Website: [princejs.vercel.app](https://princejs.vercel.app)
- ๐Ÿ“ฆ npm: [npmjs.com/package/princejs](https://www.npmjs.com/package/princejs)
- ๐Ÿ’ป GitHub: [github.com/MatthewTheCoder1218/princejs](https://github.com/MatthewTheCoder1218/princejs)
- ๐Ÿฆ Twitter: [@princejs_bun](https://twitter.com/princejs_bun)

---

**PrinceJS: ~5kB. Hono-speed. Everything included. ๐Ÿ‘‘**

*Built with โค๏ธ in Nigeria*