Skip to content

Commit 169e702

Browse files
updated version
1 parent 1cbab71 commit 169e702

File tree

4 files changed

+104
-3
lines changed

4 files changed

+104
-3
lines changed

Readme.md

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ app.listen(3000);
6262
| Feature | Import |
6363
|---------|--------|
6464
| Routing, Route Grouping, WebSockets, OpenAPI, Plugins, Lifecycle Hooks, Cookies, IP | `princejs` |
65-
| CORS, Logger, JWT, JWKS, Auth, Rate Limit, Validate, Compress, Session, API Key, Secure Headers, Timeout, Request ID, IP Restriction, Static Files | `princejs/middleware` |
65+
| 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` |
6666
| File Uploads, SSE, Streaming, In-memory Cache | `princejs/helpers` |
6767
| Cron Scheduler | `princejs/scheduler` |
6868
| JSX / SSR | `princejs/jsx` |
@@ -268,6 +268,90 @@ app.use(ipRestriction({ denyList: ["1.2.3.4"] }));
268268

269269
---
270270

271+
## ✂️ Trim Trailing Slash
272+
273+
Automatically redirect `/users/``/users` so you never get mysterious 404s from a stray trailing slash:
274+
275+
```ts
276+
import { trimTrailingSlash } from "princejs/middleware";
277+
278+
app.use(trimTrailingSlash()); // 301 by default
279+
app.use(trimTrailingSlash(302)); // or 302 temporary redirect
280+
```
281+
282+
Root `/` is never redirected. Query strings are preserved — `/search/?q=bun``/search?q=bun`.
283+
284+
---
285+
286+
## 🔀 Middleware Combinators
287+
288+
Compose complex auth rules in a single readable line.
289+
290+
### `every()` — all must pass
291+
292+
```ts
293+
import { every } from "princejs/middleware";
294+
295+
const isAdmin = async (req, next) => {
296+
if (req.user?.role !== "admin")
297+
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
298+
return next();
299+
};
300+
301+
app.get("/admin", every(auth(), isAdmin), () => ({ ok: true }));
302+
// short-circuits on first rejection — isAdmin never runs if auth() fails
303+
```
304+
305+
### `some()` — either must pass
306+
307+
```ts
308+
import { some } from "princejs/middleware";
309+
310+
// Accept a JWT token OR an API key — whichever the client sends
311+
app.get("/resource", some(auth(), apiKey({ keys: ["key_123"] })), () => ({ ok: true }));
312+
```
313+
314+
### `except()` — skip middleware for certain paths
315+
316+
```ts
317+
import { except } from "princejs/middleware";
318+
319+
// Apply auth everywhere except /health and /
320+
app.use(except(["/health", "/"], auth()));
321+
322+
app.get("/health", () => ({ ok: true })); // no auth
323+
app.get("/private", (req) => ({ user: req.user })); // auth required
324+
```
325+
326+
---
327+
328+
## 🛡️ guard()
329+
330+
Apply a validation schema to every route in a group at once — no need to repeat `validate()` on each handler:
331+
332+
```ts
333+
import { guard } from "princejs/middleware";
334+
import { z } from "zod";
335+
336+
app.group("/users", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
337+
r.post("/", (req) => ({ created: req.parsedBody.name })); // auto-validated
338+
r.put("/:id", (req) => ({ updated: req.parsedBody.name })); // auto-validated
339+
});
340+
// Bad body → 400 { error: "Validation failed", details: [...] }
341+
```
342+
343+
Also works as standalone route middleware:
344+
345+
```ts
346+
app.post(
347+
"/items",
348+
guard({ body: z.object({ name: z.string(), price: z.number() }) }),
349+
(req) => ({ created: req.parsedBody })
350+
);
351+
```
352+
353+
---
354+
271355
## 📁 Static Files
272356

273357
Serve a directory of static files. Falls through to your routes if the file doesn't exist:
@@ -537,6 +621,11 @@ import {
537621
secureHeaders,
538622
timeout,
539623
requestId,
624+
trimTrailingSlash,
625+
every,
626+
some,
627+
except,
628+
guard,
540629
} from "princejs/middleware";
541630
import { cache, upload, sse, stream } from "princejs/helpers";
542631
import { cron } from "princejs/scheduler";
@@ -559,6 +648,7 @@ app.onError((err, req, path, method) => {
559648
// ── Global middleware ─────────────────────────────────────
560649
app.use(secureHeaders());
561650
app.use(requestId());
651+
app.use(trimTrailingSlash());
562652
app.use(timeout(10000));
563653
app.use(cors());
564654
app.use(logger());
@@ -600,6 +690,16 @@ app.ws("/chat", {
600690
// ── Auth & API keys ───────────────────────────────────────
601691
app.get("/protected", auth(), (req) => ({ user: req.user }));
602692
app.get("/api", apiKey({ keys: ["key_123"] }), () => ({ ok: true }));
693+
app.get("/admin", every(auth(), async (req, next) => {
694+
if (req.user?.role !== "admin")
695+
return new Response(JSON.stringify({ error: "Forbidden" }), { status: 403 });
696+
return next();
697+
}), () => ({ admin: true }));
698+
699+
// ── Validated route group ─────────────────────────────────
700+
app.group("/items", guard({ body: z.object({ name: z.string().min(1) }) }), (r) => {
701+
r.post("/", (req) => ({ created: req.parsedBody.name }));
702+
});
603703

604704
// ── Helpers ───────────────────────────────────────────────
605705
app.get("/cached", cache(60)(() => ({ time: Date.now() })));

integration-test.sqlite

-12 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "princejs",
3-
"version": "2.2.2",
3+
"version": "2.2.3",
44
"description": "An easy and fast backend framework that is among the top three — by a 13yo developer, for developers.",
55
"main": "dist/prince.js",
66
"types": "dist/prince.d.ts",

src/prince.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -920,7 +920,8 @@ async handleFetch(req: Request): Promise<Response> {
920920
if (pathname.length > 1 && pathname.endsWith("/")) {
921921
const search = extractSearch(rawUrl);
922922
const trimmed = pathname.slice(0, -1) + (search ? `?${search}` : "");
923-
return new Response(null, { status: 301, headers: { Location: trimmed } });
923+
const status = (this.middlewares.find((m: any) => m.__trimTrailingSlash) as any)?.__trimTrailingSlash ?? 301;
924+
return new Response(null, { status, headers: { Location: trimmed } });
924925
}
925926
return this.json({ error: "Not Found" }, 404);
926927
}

0 commit comments

Comments
 (0)