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"