diff --git a/README.md b/README.md
index 85f01044..ec4763a6 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@
Overview
-Routes and middleware are added to a `Router` instance with `.use`, `.addRoute` or `.get/post/put/delete`.
+Routes and middleware are added to a `Router` instance with `.use`, `.addRoute` or `.get/post/put/delete`.
The router is then used with your web server of choice, e.g. `Deno.serve` or `Bun.serve`.
@@ -66,76 +66,116 @@ router.use(Peko.logger(console.log));
router.get("/shorthand-route", () => new Response("Hello world!"));
-router.post("/shorthand-route-ext", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, (req) => new Response(req.body));
+router.post(
+ "/shorthand-route-ext",
+ async (ctx, next) => {
+ await next();
+ console.log(ctx.request.headers);
+ },
+ (req) => new Response(req.body)
+);
router.addRoute({
- path: "/object-route",
- middleware: async (ctx, next) => { await next(); console.log(ctx.request.headers); }, // can also be array of middleware
- handler: () => new Response("Hello world!")
-})
+ path: "/object-route",
+ middleware: async (ctx, next) => {
+ await next();
+ console.log(ctx.request.headers);
+ }, // can also be array of middleware
+ handler: () => new Response("Hello world!"),
+});
-router.addRoutes([ /* array of route objects */ ])
+router.addRoutes([
+ /* array of route objects */
+]);
-Deno.serve((req) => router.handle(req))
+Deno.serve((req) => router.handle(req));
```
Types
### [**Router**](https://deno.land/x/peko/mod.ts?s=Router)
+
The main class/entrypoint of Peko.
-The `handle` method generates a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) from a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) argument via configured routes and middleware.
+The `handle` method generates a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) from a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request) argument via configured routes and middleware.
### [**Route**](https://deno.land/x/peko/mod.ts?s=Route)
-Routes are added to a `Router` and matched to a `Request` via their `path` property. Once matched, the route's `middleware` and `handlers` are invoked to process the `Request` (after global middleware on the `Router`).
+
+Routes are added to a `Router` and matched to a `Request` via their `path` property. Once matched, the route's `middleware` and `handlers` are invoked to process the `Request` (after global middleware on the `Router`).
Dynamic path parameters are supported in the `/users/:userid` syntax.
### [**RequestContext**](https://deno.land/x/peko/mod.ts?s=RequestContext)
+
An object containing request data that is passed into middleware and handlers in the `Request` process lifecycle.
The `state` property is an object designed to transfer information between middleware/handlers.
### [**Middleware**](https://deno.land/x/peko/mod.ts?s=Middleware)
+
Functions that receive `RequestContext` and `next`. They are designed to:
+
- Return a `Response` and end the `Request` processing lifecycle (e.g. returning a `401`)
- Call `await next()` to access the final response (e.g. logging)
- Edit the context's `state` (e.g. rendering geolocation to HTML)
### [**Handler**](https://deno.land/x/peko/mod.ts?s=Handler)
+
The final request handling function on a `Route`, receives `RequestContext` argument.
Must return/resolve to a `Response` (e.g. Render HTML or return JSON payload).
Recipes
-### Library utilities
+### Examples
+
Check the [examples](https://github.com/sejori/peko/tree/main/example) to see implementations of:
+
- server-side rendering Preact to HTML
- streaming server-sent events to web client
- JWT authentication middleware
- logging requests
- caching responses
+### Deno
+
+- Process 1: `deno task dev:build`
+- Process 2: `deno task dev:deno`
+
+### Wrangler (Node with Cloudflare Worker target):
+
+- `npm i`
+- Process 1: `npm run dev:build`
+- Process 2: `npm run dev:wrangler`
+
+### Bun:
+
+- `bun install`
+- Process 1: `bun dev:build`
+- Process 2: `bun dev:bun`
+
### Error handling
If no matching route is found for a request an empty 404 response is sent. If an error occurs in handling a request an empty 500 response is sent. Both of these behaviours can be overwritten with the following middleware:
```js
router.use(async (_, next) => {
- const response = await next();
- if (!response) return new Response("Would you look at that? Nothing's here!", { status: 404 });
+ const response = await next();
+ if (!response)
+ return new Response("Would you look at that? Nothing's here!", {
+ status: 404,
+ });
});
```
```js
router.use(async (_, next) => {
- try {
- await next();
- } catch(e) {
- console.log(e);
- return new Response("Oh no! An error occured :(", { status: 500 });
- }
+ try {
+ await next();
+ } catch (e) {
+ console.log(e);
+ return new Response("Oh no! An error occured :(", { status: 500 });
+ }
});
```
@@ -144,29 +184,33 @@ router.use(async (_, next) => {
In stateless computing, memory should only be used for source code and disposable cache data. Response caching ensures that we only store data that can be regenerated or refetched. The configurable `cacher` middleware provides drop in handler memoization and response caching for your routes.
```js
-router.addRoute("/get-time", Peko.cacher({ itemLifetime: 5000 }), () => new Response(Date.now()));
+router.addRoute(
+ "/get-time",
+ Peko.cacher({ itemLifetime: 5000 }),
+ () => new Response(Date.now())
+);
```
The cacher stores response items in memory by default, but it can be extended to use any key value storage by supplying the `store` options parameter (e.g. Cloudflare Workers KV).
```js
-import { Router, CacheItem, cacher } from "https://deno.land/x/peko/mod.ts"
+import { Router, CacheItem, cacher } from "https://deno.land/x/peko/mod.ts";
const router = new Router();
-const itemMap: Map = new Map()
+const itemMap: Map = new Map();
router.addRoute("/get-time", {
- middleware: cacher({
- itemLifetime: 5000,
- store: {
- get: (key) => itemMap.get(key),
- set: (key, value) => itemMap.set(key, value),
- delete: (key) => itemMap.delete(key)
- }
- }),
- handler: () => new Response(Date.now())
-})
+ middleware: cacher({
+ itemLifetime: 5000,
+ store: {
+ get: (key) => itemMap.get(key),
+ set: (key, value) => itemMap.set(key, value),
+ delete: (key) => itemMap.delete(key),
+ },
+ }),
+ handler: () => new Response(Date.now()),
+});
```
Deployment
@@ -178,12 +222,14 @@ router.addRoute("/get-time", {
PR to add your project 🙌
-### [shineponics.org](https://shineponics.org)
+### [shineponics.deno.dev](https://shineponics.deno.dev)
+
- **Stack:** React, Google Cloud Platform
- **Features:** Google Sheet analytics, GCP email list, Markdown rendering
- [source](https://github.com/shine-systems/shineponics/blob/main/server.ts)
-### [thesebsite.com](https://thesebsite.com)
+### [thesebsite.deno.dev](https://thesebsite.deno.dev)
+
- **Stack:** HTML5
- **Features:** UI TS scripts transpiled to JS and cached for browser
- [source](https://github.com/sebringrose/peko/blob/main/examples/auth/app.ts)
@@ -211,4 +257,5 @@ Because stateless apps can "cold-start" it is important to keep their codebases
Note: In reality a single app instance will serve multiple requests, we just can't guarantee it. This is why caching is still an effective optimization strategy but in-memory user sessions are not an effective authentication strategy.
## Credits:
+
Chick logo from [Twemoji](https://github.com/twitter/twemoji)
diff --git a/deno.json b/deno.json
index 83335551..2226a64e 100644
--- a/deno.json
+++ b/deno.json
@@ -4,7 +4,6 @@
"version": "2.2.0",
"exports": "./mod.ts",
"imports": {
- "esbuild": "https://deno.land/x/esbuild@v0.23.0/mod.js",
"htm/preact": "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string",
"preact": "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string",
"preact/hooks": "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string",
diff --git a/deno.lock b/deno.lock
index 639c8fb0..1e822147 100644
--- a/deno.lock
+++ b/deno.lock
@@ -2,6 +2,7 @@
"version": "3",
"packages": {
"specifiers": {
+ "npm:esbuild@0.23.0": "npm:esbuild@0.23.0",
"npm:htm@^3.1.1": "npm:htm@3.1.1",
"npm:ts-node@^10.9.2": "npm:ts-node@10.9.2_@types+node@18.16.19_typescript@5.4.2"
},
@@ -12,6 +13,102 @@
"@jridgewell/trace-mapping": "@jridgewell/trace-mapping@0.3.9"
}
},
+ "@esbuild/aix-ppc64@0.23.0": {
+ "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==",
+ "dependencies": {}
+ },
+ "@esbuild/android-arm64@0.23.0": {
+ "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==",
+ "dependencies": {}
+ },
+ "@esbuild/android-arm@0.23.0": {
+ "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==",
+ "dependencies": {}
+ },
+ "@esbuild/android-x64@0.23.0": {
+ "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==",
+ "dependencies": {}
+ },
+ "@esbuild/darwin-arm64@0.23.0": {
+ "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==",
+ "dependencies": {}
+ },
+ "@esbuild/darwin-x64@0.23.0": {
+ "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==",
+ "dependencies": {}
+ },
+ "@esbuild/freebsd-arm64@0.23.0": {
+ "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==",
+ "dependencies": {}
+ },
+ "@esbuild/freebsd-x64@0.23.0": {
+ "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-arm64@0.23.0": {
+ "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-arm@0.23.0": {
+ "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-ia32@0.23.0": {
+ "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-loong64@0.23.0": {
+ "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-mips64el@0.23.0": {
+ "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-ppc64@0.23.0": {
+ "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-riscv64@0.23.0": {
+ "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-s390x@0.23.0": {
+ "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==",
+ "dependencies": {}
+ },
+ "@esbuild/linux-x64@0.23.0": {
+ "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==",
+ "dependencies": {}
+ },
+ "@esbuild/netbsd-x64@0.23.0": {
+ "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==",
+ "dependencies": {}
+ },
+ "@esbuild/openbsd-arm64@0.23.0": {
+ "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==",
+ "dependencies": {}
+ },
+ "@esbuild/openbsd-x64@0.23.0": {
+ "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==",
+ "dependencies": {}
+ },
+ "@esbuild/sunos-x64@0.23.0": {
+ "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==",
+ "dependencies": {}
+ },
+ "@esbuild/win32-arm64@0.23.0": {
+ "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==",
+ "dependencies": {}
+ },
+ "@esbuild/win32-ia32@0.23.0": {
+ "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==",
+ "dependencies": {}
+ },
+ "@esbuild/win32-x64@0.23.0": {
+ "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==",
+ "dependencies": {}
+ },
"@jridgewell/resolve-uri@3.1.2": {
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dependencies": {}
@@ -67,6 +164,35 @@
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"dependencies": {}
},
+ "esbuild@0.23.0": {
+ "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==",
+ "dependencies": {
+ "@esbuild/aix-ppc64": "@esbuild/aix-ppc64@0.23.0",
+ "@esbuild/android-arm": "@esbuild/android-arm@0.23.0",
+ "@esbuild/android-arm64": "@esbuild/android-arm64@0.23.0",
+ "@esbuild/android-x64": "@esbuild/android-x64@0.23.0",
+ "@esbuild/darwin-arm64": "@esbuild/darwin-arm64@0.23.0",
+ "@esbuild/darwin-x64": "@esbuild/darwin-x64@0.23.0",
+ "@esbuild/freebsd-arm64": "@esbuild/freebsd-arm64@0.23.0",
+ "@esbuild/freebsd-x64": "@esbuild/freebsd-x64@0.23.0",
+ "@esbuild/linux-arm": "@esbuild/linux-arm@0.23.0",
+ "@esbuild/linux-arm64": "@esbuild/linux-arm64@0.23.0",
+ "@esbuild/linux-ia32": "@esbuild/linux-ia32@0.23.0",
+ "@esbuild/linux-loong64": "@esbuild/linux-loong64@0.23.0",
+ "@esbuild/linux-mips64el": "@esbuild/linux-mips64el@0.23.0",
+ "@esbuild/linux-ppc64": "@esbuild/linux-ppc64@0.23.0",
+ "@esbuild/linux-riscv64": "@esbuild/linux-riscv64@0.23.0",
+ "@esbuild/linux-s390x": "@esbuild/linux-s390x@0.23.0",
+ "@esbuild/linux-x64": "@esbuild/linux-x64@0.23.0",
+ "@esbuild/netbsd-x64": "@esbuild/netbsd-x64@0.23.0",
+ "@esbuild/openbsd-arm64": "@esbuild/openbsd-arm64@0.23.0",
+ "@esbuild/openbsd-x64": "@esbuild/openbsd-x64@0.23.0",
+ "@esbuild/sunos-x64": "@esbuild/sunos-x64@0.23.0",
+ "@esbuild/win32-arm64": "@esbuild/win32-arm64@0.23.0",
+ "@esbuild/win32-ia32": "@esbuild/win32-ia32@0.23.0",
+ "@esbuild/win32-x64": "@esbuild/win32-x64@0.23.0"
+ }
+ },
"htm@3.1.1": {
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
"dependencies": {}
diff --git a/package.json b/package.json
index 5aac39b6..717a4fec 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
"profile:bun": "bun run scripts/bun/profile.ts",
"profile:wrangler": "node --loader ts-node/esm scripts/wrangler/profile.ts",
"start:wrangler": "wrangler dev scripts/wrangler/testApp.ts",
- "dev:build": "esbuild --bundle --sourcemap --target=es2020 --platform=browser --format=esm --outdir=./example/preactSSR/dist/pages --external:esbuild --external:htm/preact --external:preact --external:preact/hooks --external:preact-render-to-string --watch ./example/preactSSR/src/pages/*.ts",
+ "dev:build": "esbuild --bundle --sourcemap --target=es2020 --platform=browser --format=esm --outdir=./example/preactSSR/dist/pages --external:esbuild --external:htm/preact --external:preact --external:preact/hooks --external:preact-render-to-string --watch=forever ./example/preactSSR/src/pages/*.ts",
"dev:deno": "deno run -A --watch scripts/deno/main.ts",
"dev:bun": "bun run --watch scripts/bun/main.ts",
"dev:wrangler": "wrangler dev scripts/wrangler/main.ts"