Skip to content

Commit

Permalink
feat: access route parameters (#29)
Browse files Browse the repository at this point in the history
* feat: route params available on runtime

* chore: explicit core exports for better tree-shaking support

* feat: new params() util

* chore: make params helpers more generic

* doc: add route parameters recipe

* test(hono): route params

* test(hattip): route params

* test(h3): route params

* test(express): route params

* test(fastify): route params

* chore(cloudflare): route params

* chore(webroute): route params

* doc: complete route params

* chore: create params handler in example

* doc: finish route params doc

* chore(ci): add delay for wrangler tests
  • Loading branch information
magne4000 authored Sep 11, 2024
1 parent 5952802 commit 3a7d500
Show file tree
Hide file tree
Showing 59 changed files with 694 additions and 190 deletions.
16 changes: 10 additions & 6 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { defineConfig } from "vitepress";
import { transformerTwoslash } from "@shikijs/vitepress-twoslash";
import type { ModuleResolutionKind } from "typescript";
import { defineConfig } from "vitepress";

// https://vitepress.dev/reference/site-config
export default defineConfig({
Expand Down Expand Up @@ -28,7 +28,7 @@ export default defineConfig({
// https://vitepress.dev/reference/default-theme-config
nav: [
{ text: "Guide", link: "/guide/introduction" },
{ text: "Examples", link: "/examples/context-middleware" },
{ text: "Recipes", link: "/recipes/context-middleware" },
],

sidebar: [
Expand All @@ -50,19 +50,23 @@ export default defineConfig({
],
},
{
text: "Examples",
text: "Recipes",
items: [
{
text: "Updating the context",
link: "/examples/context-middleware",
link: "/recipes/context-middleware",
},
{
text: "Updating the headers",
link: "/examples/headers-middleware",
link: "/recipes/headers-middleware",
},
{
text: "Return an early response",
link: "/examples/guard-middleware",
link: "/recipes/guard-middleware",
},
{
text: "Using route parameters",
link: "/recipes/params-handler",
},
],
},
Expand Down
18 changes: 11 additions & 7 deletions docs/definitions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ A function that returns a [Response](https://developer.mozilla.org/en-US/docs/We
It will usually be associated with a _route_.

```ts twoslash
export interface UniversalHandler<Context> {
interface UniversalHandler<Context> {
(request: Request, context: Context): Response | Promise<Response>;
}
```
Expand All @@ -20,15 +20,19 @@ A function that alters the [Context](#context) or Response. Can also return an e

It will usually _NOT_ be associated with a _route_.

Check the [examples](/examples/context-middleware) for details.
Check the [recipes](/recipes/context-middleware) for details.

```ts twoslash
export interface UniversalMiddleware<InContext, OutContext> {
type Awaitable<T> = T | Promise<T>;

interface UniversalMiddleware<InContext, OutContext> { // [!code focus:9]
(request: Request, context: InContext):
| Response | Promise<Response> // Can return an early Response
| void | Promise<void> // Can return nothing
| OutContext | Promise<OutContext> // Can return a new context. Ensures type-safe context representation
| ((response: Response) => Response | Promise<Response>); // Can return a function that manipulates the Response
Awaitable<
| Response // Can return an early Response
| OutContext // Can return a new context. Ensures type-safe context representation
| ((response: Response) => Awaitable<Response>) // Can return a function that manipulates the Response
| void | undefined // Can return nothing
>;
}
```

Expand Down
6 changes: 3 additions & 3 deletions docs/guide/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This suite allows you to create and bundle server-related code that works with a
ensuring consistency and reducing duplication.

Efficiently develop a wide range of middleware and handlers, including but not limited to:
- [HTTP Header Middleware](/examples/headers-middleware): Easily modify or add headers to outgoing responses.
- [Context-Altering Middleware](/examples/context-middleware): Enhance request [context](/definitions#context), such as adding user authentication properties for logged-in users.
- [Guard Middleware](/examples/guard-middleware): Enforce request validation, like checking for an Authentication header before allowing further processing.
- [HTTP Header Middleware](/recipes/headers-middleware): Easily modify or add headers to outgoing responses.
- [Context-Altering Middleware](/recipes/context-middleware): Enhance request [context](/definitions#context), such as adding user authentication properties for logged-in users.
- [Guard Middleware](/recipes/guard-middleware): Enforce request validation, like checking for an Authentication header before allowing further processing.
- Web Standard Handlers: Implement handlers that adhere to the Request/Response model defined by web standards.
2 changes: 1 addition & 1 deletion docs/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ In this step, we will create a simple middleware that returns an early response
```ts twoslash
// src/middlewares/demo.middleware.ts

import type { Get, UniversalMiddleware } from "universal-middleware";
import type { Get, UniversalMiddleware } from "@universal-middleware/core";

interface Config {
header: string;
Expand Down
4 changes: 2 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ hero:
text: Guide
link: /guide/introduction
- theme: alt
text: Examples
link: /examples/context-middleware
text: Recipes
link: /recipes/context-middleware

features:
- title: One middleware, multiple adapters
Expand Down
2 changes: 1 addition & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
"hono": "catalog:",
"typescript": "^5.5.4",
"universal-middleware": "workspace:*",
"vitepress": "^1.3.3"
"vitepress": "^1.3.4"
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
119 changes: 119 additions & 0 deletions docs/recipes/params-handler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Using route parameters

Most adapters natively support route parameters (also called _parametric path_ or _path parameters_) such as `/hello/:name`.
`@universal-middleware/core` provides the `params` helper to universally retrieve those.

We recommend to follow this next example when using route parameters:

<<< @/../examples/tool/src/handlers/params.handler.ts

> [!NOTE]
> For servers supporting route parameters (`app.get("/user/:name", myHandler())`), the parameters are available under `runtime.params`.
>
> For other adapters (`app.get("/user/*", myHandler({ route: "/user/:name" }))`), the 3rd argument of `params` helper must be present and not _undefined_.
> Then parameters are extracted with [regexparam](https://github.com/lukeed/regexparam).
After bundling and publishing this middleware, one can then use this middleware as follows:

::: code-group

```ts twoslash [hono.ts]
import { Hono } from "hono";
import paramHandler from "@universal-middleware-examples/tool/params-handler-hono";

const app = new Hono();

app.get("/user/:name", paramHandler());

export default app;
```

```ts twoslash [h3.ts]
import { createApp, createRouter } from "h3";
import paramHandler from "@universal-middleware-examples/tool/params-handler-h3";
import { universalOnBeforeResponse } from "@universal-middleware/h3";

const app = createApp({
// /!\ This is required for universal-middleware to operate properly
onBeforeResponse: universalOnBeforeResponse,
});

const router = createRouter();

router.get("/user/:name", paramHandler());

app.use(router);

export default app;
```

```ts twoslash [hattip.ts]
import { createRouter } from "@hattip/router";
import paramHandler from "@universal-middleware-examples/tool/params-handler-hattip";

const app = createRouter();

app.get("/user/:name", paramHandler());

const hattipHandler = app.buildHandler();

export default hattipHandler;
```

```ts twoslash [cloudflare-worker.ts]
import paramsHandler from "@universal-middleware-examples/tool/params-handler";
import { createHandler } from "@universal-middleware/cloudflare";
import { pipe } from "@universal-middleware/core";

const paramsHandlerInstance = paramsHandler({
// Mandatory when targeting Cloudflare Worker
route: "/user/:name",
});

// Cloudflare Workers have no native routing support.
// We recommend using Hono as it fully supports Cloudflare Worker.
const wrapped = pipe(
(request, ctx, runtime) => {
const url = new URL(request.url);
// intercept `/user/*` routes with this handler
if (url.pathname.startsWith("/user/")) {
return paramsHandlerInstance(request, ctx, runtime);
}
},
// Other handlers
);

export default createHandler(() => wrapped)();
```

```ts twoslash [cloudflare-pages]
// functions/user/[name].ts

import paramHandler from "@universal-middleware-examples/tool/params-handler-cloudflare-pages";

export const onRequest = paramHandler();
```

```ts twoslash [express.ts]
import paramHandler from "@universal-middleware-examples/tool/params-handler-express";
import express from "express";

const app = express();

app.get("/user/:name", paramHandler());

export default app;
```

```ts twoslash [fastify.ts]
import paramHandler from "@universal-middleware-examples/tool/params-handler-fastify";
import fastify from "fastify";

const app = fastify();

app.get("/user/:name", paramHandler());

export default app;
```

:::
46 changes: 46 additions & 0 deletions examples/tool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,51 @@
"types": "./dist/middlewares/universal-cloudflare-pages-middleware-guard.middleware.d.ts",
"import": "./dist/middlewares/universal-cloudflare-pages-middleware-guard.middleware.js",
"default": "./dist/middlewares/universal-cloudflare-pages-middleware-guard.middleware.js"
},
"./params-handler": {
"types": "./dist/params.d.ts",
"import": "./dist/params.js",
"default": "./dist/params.js"
},
"./params-handler-hono": {
"types": "./dist/handlers/universal-hono-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-hono-handler-params.handler.js",
"default": "./dist/handlers/universal-hono-handler-params.handler.js"
},
"./params-handler-express": {
"types": "./dist/handlers/universal-express-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-express-handler-params.handler.js",
"default": "./dist/handlers/universal-express-handler-params.handler.js"
},
"./params-handler-hattip": {
"types": "./dist/handlers/universal-hattip-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-hattip-handler-params.handler.js",
"default": "./dist/handlers/universal-hattip-handler-params.handler.js"
},
"./params-handler-webroute": {
"types": "./dist/handlers/universal-webroute-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-webroute-handler-params.handler.js",
"default": "./dist/handlers/universal-webroute-handler-params.handler.js"
},
"./params-handler-fastify": {
"types": "./dist/handlers/universal-fastify-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-fastify-handler-params.handler.js",
"default": "./dist/handlers/universal-fastify-handler-params.handler.js"
},
"./params-handler-h3": {
"types": "./dist/handlers/universal-h3-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-h3-handler-params.handler.js",
"default": "./dist/handlers/universal-h3-handler-params.handler.js"
},
"./params-handler-cloudflare-worker": {
"types": "./dist/handlers/universal-cloudflare-worker-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-cloudflare-worker-handler-params.handler.js",
"default": "./dist/handlers/universal-cloudflare-worker-handler-params.handler.js"
},
"./params-handler-cloudflare-pages": {
"types": "./dist/handlers/universal-cloudflare-pages-handler-params.handler.d.ts",
"import": "./dist/handlers/universal-cloudflare-pages-handler-params.handler.js",
"default": "./dist/handlers/universal-cloudflare-pages-handler-params.handler.js"
}
},
"author": "Joël Charles <[email protected]>",
Expand All @@ -220,6 +265,7 @@
"@hono/node-server": "^1.12.0",
"@swc/core": "^1.7.11",
"@types/node": "catalog:",
"@universal-middleware/core": "workspace:*",
"rimraf": "^6.0.0",
"tsup": "^8.2.4",
"typescript": "^5.5.4",
Expand Down
2 changes: 1 addition & 1 deletion examples/tool/src/handlers/handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Get, UniversalHandler } from "universal-middleware";
import type { Get, UniversalHandler } from "@universal-middleware/core";

const handler: Get<[], UniversalHandler> = () => (_request, ctx) => {
return new Response(`context: ${JSON.stringify(ctx)}`, {
Expand Down
23 changes: 23 additions & 0 deletions examples/tool/src/handlers/params.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { UniversalHandler } from "@universal-middleware/core";
import { params } from "@universal-middleware/core";

interface RouteParamOption {
route?: string;
}

const handler = ((options?) => (request, _context, runtime) => {
const myParams = params(request, runtime, options?.route);

if (myParams === null || !myParams.name) {
// Provide a useful error message to the user
throw new Error(
"A route parameter named `:name` is required. " +
"You can set your server route as `/user/:name`, or use the `route` option of this middleware " +
"to achieve the same purpose.",
);
}

return new Response(`User name is: ${myParams.name}`);
}) satisfies (options?: RouteParamOption) => UniversalHandler;

export default handler;
2 changes: 1 addition & 1 deletion examples/tool/src/middlewares/context.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/middlewares/context.middleware.ts

import type { Get, UniversalMiddleware } from "universal-middleware";
import type { Get, UniversalMiddleware } from "@universal-middleware/core";

const contextMiddleware = ((value) => (request, ctx) => {
// Return the new universal context, thus keeping complete type safety
Expand Down
2 changes: 1 addition & 1 deletion examples/tool/src/middlewares/guard.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/middlewares/guard.middleware.ts

import type { Get, UniversalMiddleware } from "universal-middleware";
import type { Get, UniversalMiddleware } from "@universal-middleware/core";

interface User {
id: string;
Expand Down
2 changes: 1 addition & 1 deletion examples/tool/src/middlewares/headers.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// src/middlewares/headers.middleware.ts

import type { Get, UniversalMiddleware } from "universal-middleware";
import type { Get, UniversalMiddleware } from "@universal-middleware/core";

// This middleware will add a `X-Universal-Hello` header to all responses
const headersMiddleware = (() => (request, ctx) => {
Expand Down
1 change: 1 addition & 0 deletions examples/tool/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default defineConfig([
{
entry: {
dummy: "./src/handlers/handler.ts",
params: "./src/handlers/params.handler.ts",
"middlewares/context": "./src/middlewares/context.middleware.ts",
"middlewares/headers": "./src/middlewares/headers.middleware.ts",
"middlewares/guard": "./src/middlewares/guard.middleware.ts",
Expand Down
4 changes: 4 additions & 0 deletions packages/adapter-cloudflare/functions/user/[name].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { routeParamHandler } from "@universal-middleware/tests/utils";
import { createPagesFunction } from "../../src/index.js";

export const onRequest = createPagesFunction(routeParamHandler)();
Loading

0 comments on commit 3a7d500

Please sign in to comment.