Skip to content

Commit

Permalink
Merge pull request #176 from sejori/router-rework
Browse files Browse the repository at this point in the history
Router rework
  • Loading branch information
sejori authored Aug 3, 2023
2 parents 9afb1c0 + 5f89632 commit 5fd1130
Show file tree
Hide file tree
Showing 38 changed files with 557 additions and 720 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/deno.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@ jobs:
uses: actions/checkout@v3

- name: Setup Deno
# uses: denoland/setup-deno@v1
uses: denoland/setup-deno@004814556e37c54a2f6e31384c9e18e983317366
uses: denoland/setup-deno@v1
with:
deno-version: v1.x

Expand All @@ -41,5 +40,5 @@ jobs:
- name: Run tests
run: deno task test

- name: Run profile
run: deno task profile
# - name: Run profile
# run: deno task profile
103 changes: 89 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,28 @@
<p align="center">
<span>
&nbsp;
<a href="https://github.com/sebringrose/peko/blob/main/overview.md#server">
Server
<a href="#Types">
Types
</a>
&nbsp;
</span>
<span>
&nbsp;
<a href="https://github.com/sebringrose/peko/blob/main/overview.md#routing">
<a href="#routing">
Routing
</a>
&nbsp;
</span>
<span>
&nbsp;
<a href="https://github.com/sebringrose/peko/blob/main/overview.md#request-handling">
<a href="#request-handling">
Request handling
</a>
&nbsp;
</span>
<span>
&nbsp;
<a href="https://github.com/sebringrose/peko/blob/main/overview.md#response-caching">
<a href="#response-caching">
Response caching
</a>
&nbsp;
Expand Down Expand Up @@ -64,21 +64,98 @@

- <strong>Community-driven</strong> - Popular tool integrations + contributions encouraged

<h2>Getting started</h2>
<h1>Overview</h1>

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`!

```js
import * as Peko from "https://deno.land/x/peko/mod.ts";
// import from ".../peko/lib/Server.ts" for featherweight mode
import * as Peko from "https://deno.land/x/peko/mod.ts";

const router = new Peko.Router();

router.use(Peko.logger(console.log));

const server = new Peko.Server();
router.get("/shorthand-route", () => new Response("Hello world!"));

server.use(Peko.logger(console.log));
router.post("/shorthand-route-ext", async (ctx, next) => { await next(); console.log(ctx.request.headers); }, (req) => new Response(req.body));

server.get("/hello", () => new Response("Hello world!"));
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!")
})

server.listen(7777, () => console.log("Peko server started - let's go!"));
router.addRoutes([ /* array of route objects */ ])

Deno.serve((req) => router.requestHandler(req))
```

<h2 id="types">Types</h2>

### [**Router**](https://deno.land/x/peko/mod.ts?s=Router)
The main class of Peko, provides `requestHandler` method to generate `Response` from `Request` via configured routes and middleware.

### [**Route**](https://deno.land/x/peko/mod.ts?s=Route)
Objects with `path`, `method`, `middleware`, and `handler` properties. Requests are matched to a regex generated from the given path. Dynamic parameters are supported in the `/users/:userid` syntax.

### [**RequestContext**](https://deno.land/x/peko/mod.ts?s=RequestContext)
An object containing `url`, `params` and `state` properties that is provided to all middleware and handler functions associated to a router or matched route.

### [**Middleware**](https://deno.land/x/peko/mod.ts?s=Middleware)
Functions that receives a RequestContext and a next fcn. Should update `ctx.state`, perform side-effects or return a response.

### [**Handler**](https://deno.land/x/peko/mod.ts?s=Handler)
The final request handling function on a `Route`. Must generate and return a response using the provided request context.

<h2 id="request-handling">Request handling</h2>

Each route must have a <code>handler</code> function that generates a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Upon receiving a request the `Server` will construct a [RequestContext](https://deno.land/x/peko/server.ts?s=RequestContext) and cascade it through any global middleware, then route middleware and finally the route handler. Global and route middleware are invoked in the order they are added. If a response is returned by any middleware along the chain no subsequent middleware/handler will run.

Peko comes with a library of utilities, middleware and handlers for common route use-cases, such as:
- server-side-rendering
- opening WebSockets/server-sent events
- JWT signing/verifying & authentication
- logging
- caching

See `handlers`, `mmiddleware` or `utils` for source, or dive into `examples` for demo implementations.

The second argument to any middleware is the `next` fcn. This returns a promise that resolves to the first response returned by any subsequent middleware/handler. This is useful for error-handling as well as post-response operations such as editing headers or logging. See the below snippet or `middleware/logger.ts` for examples.

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 });
});
```

```js
router.use(async (_, next) => {
try {
await next();
} catch(e) {
console.log(e);
return new Response("Oh no! An error occured :(", { status: 500 });
}
});
```

<h2 id="response-caching">Response caching</h2>

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. Peko provides a `ResponseCache` utility for this with configurable item lifetime. The `cacher` middleware wraps it and provides drop in handler memoization and response caching for your routes.

```js
const cache = new Peko.ResponseCache({ lifetime: 5000 });

router.addRoute("/do-stuff", Peko.cacher(cache), () => new Response(Date.now()));
```

And that's it! Check out the API docs for deeper info. Otherwise happy coding 🤓

<h2>App showcase</h2>

PR to add your project 🙌
Expand Down Expand Up @@ -122,5 +199,3 @@ The modern JavaScript edge rocks because the client-server gap practically disap
This is made possible by engines such as Deno that are built to the [ECMAScript](https://tc39.es/) specification</a> (support for URL module imports is the secret sauce). UI libraries like [Preact](https://github.com/preactjs/preact) combined with [htm](https://github.com/developit/htm) offer lightning fast client-side hydration with a browser-friendly markup syntax. Deno also has native TypeScript support, a rich runtime API and loads of community tools for your back-end needs.

If you are interested in contributing please submit a PR or get in contact ^^

Read `overview.md` for a more detailed guide on using Peko.
16 changes: 8 additions & 8 deletions examples/auth/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as Peko from "https://deno.land/x/peko/mod.ts"
import * as Peko from "../../mod.ts" // "https://deno.land/x/peko/mod.ts"

const server = new Peko.Server()
const html = String
const router = new Peko.Router()
const crypto = new Peko.Crypto("SUPER_SECRET_KEY_123") // <-- replace from env
const user = { // <-- replace with db / auth provider query
username: "test-user",
Expand All @@ -13,8 +14,8 @@ const validateUser = async (username: string, password: string) => {
&& await crypto.hash(password) === user.password
}

server.use(Peko.logger(console.log))
server.post("/login", async (ctx) => {
router.use(Peko.logger(console.log))
router.post("/login", async (ctx) => {
const { username, password } = await ctx.request.json()

if (!await validateUser(username, password)) {
Expand All @@ -37,14 +38,13 @@ server.post("/login", async (ctx) => {
})
})

server.get(
router.get(
"/verify",
Peko.authenticator(crypto),
() => new Response("You are authenticated!")
)

const html = String
server.get("/", Peko.ssrHandler(() => html`<!doctype html>
router.get("/asdf", Peko.ssrHandler(() => html`<!doctype html>
<html lang="en">
<head>
<title>Peko auth example</title>
Expand Down Expand Up @@ -132,4 +132,4 @@ server.get("/", Peko.ssrHandler(() => html`<!doctype html>
</html>
`))

server.listen()
Deno.serve((req) => router.requestHandler(req))
16 changes: 8 additions & 8 deletions examples/preact/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { Server, logger } from "https://deno.land/x/peko/mod.ts"
import { Router, logger } from "../../mod.ts" //"https://deno.land/x/peko/mod.ts"
import pages from "./routes/pages.ts"
import assets from "./routes/assets.ts"
import APIs from "./routes/APIs.ts"

// initialize server
const server = new Server()
server.use(logger(console.log))
const router = new Router()
router.use(logger(console.log))

// SSR'ed app page routes
server.addRoutes(pages)
router.addRoutes(pages)

// Static assets
server.addRoutes(assets)
router.addRoutes(assets)

// Custom API functions
server.addRoutes(APIs)
router.addRoutes(APIs)

// Start Peko server :^)
server.listen()
// Start Deno server with Peko router :^)
Deno.serve((req) => router.requestHandler(req))
2 changes: 1 addition & 1 deletion examples/preact/routes/APIs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
RequestContext,
Route,
sseHandler
} from "https://deno.land/x/peko/mod.ts"
} from "../../../mod.ts"

const demoEventTarget = new EventTarget()
setInterval(() => {
Expand Down
2 changes: 1 addition & 1 deletion examples/preact/routes/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
staticHandler,
cacher,
ResponseCache
} from "https://deno.land/x/peko/mod.ts"
} from "../../../mod.ts"

import { recursiveReaddir } from "https://deno.land/x/[email protected]/mod.ts"
import { fromFileUrl } from "https://deno.land/[email protected]/path/mod.ts"
Expand Down
2 changes: 1 addition & 1 deletion examples/preact/routes/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
ssrHandler,
cacher,
ResponseCache
} from "https://deno.land/x/peko/mod.ts"
} from "../../../mod.ts"

import { renderToString } from "https://npm.reversehttp.com/preact,preact/hooks,htm/preact,preact-render-to-string"

Expand Down
5 changes: 3 additions & 2 deletions examples/preact/src/components/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ const Layout = ({ navColor, navLink, children }) => {
<nav style=${navStyle(navColor)}>
<div class="container align-center">
<img height="200px" width="1000px" style="max-width:100%; margin: 1rem;" src="/assets/logo_dark_alpha.webp" alt="peko-chick" />
<h1 style="text-align: center;">Featherweight <a href="/${navLink}" style=${navLinkStyle}>apps</a> on the stateless edge</h1>
<h1 style="text-align: center;">Featherweight HTTP routing and utilities</h1>
<h2 style="text-align: center;">for <a href="/${navLink}" style=${navLinkStyle}>apps</a> on the stateless edge 🐣⚡</h2>
</div>
</nav>
<main style="padding: 1rem;" class="container">
Expand All @@ -26,7 +27,7 @@ const Layout = ({ navColor, navLink, children }) => {
<a style=${footerLinkStyle} href="/">Home</a>
<a style=${footerLinkStyle} href="/about">About</a>
</div>
<p style="margin: 10px; text-align: center">Made by <a href="https://thesebsite.com">Seb R</a></p>
<p style="margin: 10px; text-align: center">Made by <a href="https://thesebsite.com">Sejori</a></p>
</footer>
`
}
Expand Down
14 changes: 11 additions & 3 deletions examples/preact/src/pages/Home.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ const Home = () => {
<${Layout} navLink="about" navColor="#101727">
<h2>Features</h2>
<ul>
<li>Simple and familiar syntax, built on top of Deno's <a href="https://deno.land/std/http/server.ts?s=Server">std/http</a>.</li>
<li>Simple and familiar syntax, supports any modern JS/TS environment.</li>
<li>Library of request <a href="#handlers">handlers</a>, <a href="#middleware">middleware</a> and <a href="#utils">utils</a>.</li>
<li>Cascades <a target="_blank" href="https://github.com/sebringrose/peko/blob/main/server.ts">Request Context</a> through middleware stack for data flow and post-response operations.</li>
<li>100% TypeScript complete with tests.</li>
</ul>
<h2>Guides</h2>
<ol>
<li><a href="https://github.com/sebringrose/peko/blob/main/react.md">How to build a full-stack React application with Peko and Deno</a></li>
<li>Want to build a lightweight HTML or Preact app? Check out the <a href="https://github.com/sebringrose/peko/blob/main/examples">examples</a>!</li>
</ol>
<div style="display: flex; justify-content: space-between; flex-wrap: wrap;">
<div>
<h2 id="handlers">Handlers</h2>
Expand All @@ -36,8 +42,10 @@ const Home = () => {
<div>
<h2 id="utils">Utils</h2>
<ul>
<li><a target="_blank" href="https://github.com/sebringrose/peko/blob/main/middleware/Crypto.ts">Crypto - JWT/hashing</a></li>
<li><a target="_blank" href="https://github.com/sebringrose/peko/blob/main/middleware/ResponseCache.ts">Response cache</a></li>
<li><a target="_blank" href="https://github.com/sebringrose/peko/blob/main/utils/Crypto.ts">Crypto - JWT/hashing</a></li>
<li><a target="_blank" href="https://github.com/sebringrose/peko/blob/main/utils/ResponseCache.ts">Response cache</a></li>
<li><a target="_blank" href="https://github.com/sebringrose/peko/blob/main/utils/Profiler.ts">Profiler</a></li>
<li><a target="_blank" href="https://github.com/sebringrose/peko/blob/main/utils/helpers.ts">routesFromDir</a></li>
</ul>
</div>
</div>
Expand Down
Loading

0 comments on commit 5fd1130

Please sign in to comment.