-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
switch all routes to fresh style syntax add `Getting Started with @http` blog add hashbangs to scripts
- Loading branch information
Showing
20 changed files
with
846 additions
and
134 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
<!DOCTYPE html> | ||
<html lang="en-GB"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>Jollytoad</title><link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/missing.min.css" integrity="sha384-se/UYQCQ0CMlLo1I5DcMmgR8t9hjCEpTpjPu7JWzT6M4wbxzI078hgX0pxTLyyMm" crossorigin="anonymous"/><link rel="stylesheet" href="/prism.css"/><link rel="stylesheet" href="/main.css"/><script src="https://unpkg.com/[email protected]" integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC" crossorigin="anonymous"></script><script src="https://unpkg.com/[email protected]/dist/ext/sse.js" integrity="sha384-jlVlI/i5K5APUIz8cxowC1/FsCEZgsrg126wue89Np9N75pQdAzqkYYP+jsUi43W" crossorigin="anonymous"></script><script src="/app.js" type="module"></script></head><body><header><h1><a href="/">The home of Jollytoad</a></h1></header><main><h1>Getting Started with <strong>@http functions</strong></h1> | ||
<p><a href="https://jsr.io/@http">@http functions</a> is not a framework, it's a library of | ||
functions that work well together, but also with other frameworks (that use | ||
standard web APIs).</p> | ||
<p>At present there is no <code>init</code> command to create a template app.</p> | ||
<p>I feel the main benefit of using <code>@http</code> functions is the transparency of the | ||
code, it is deliberated designed so that the runtime path is simple and easy to | ||
follow. Indeed I encourage you to look inside the functions and get a feel for | ||
what they do and how they work, they should hopefully be clear, and if not | ||
please feel free to raise an issue to help improve that situation.</p> | ||
<p>So with this in mind, this guide is a like a | ||
<a href="https://www.linuxfromscratch.org/">Linux From Scratch</a> but for a web app, using | ||
this library of <code>@http</code> functions.</p> | ||
<h2>Assumptions</h2> | ||
<p>I'm assuming you have <a href="https://docs.deno.com/runtime/manual">Deno installed</a> and | ||
your favourite IDE ready to work with it.</p> | ||
<h2>What are we going to do?</h2> | ||
<ul> | ||
<li>Create the basic project structure</li> | ||
<li>Add a request handler for <code>/</code></li> | ||
<li>Generate a router from filesystem based routing structure</li> | ||
<li>Add static file routing</li> | ||
<li>Add production and development entry points</li> | ||
<li>Add a route with a pattern</li> | ||
<li>Add a path syntax mapper</li> | ||
<li>Add JSX support</li> | ||
</ul> | ||
<h2>Project structure</h2> | ||
<p>First create a new folder for your project, and <code>cd</code> into it.</p> | ||
<p>I put all utility scripts into <code>scripts</code>, and the app itself into <code>app</code>, this is | ||
just a convention I like, you can structure it however you want.</p> | ||
<pre><code class="language-sh">mkdir app scripts | ||
</code></pre> | ||
<h2>Filesystem based routing</h2> | ||
<p>This is completely optional when using <code>@http</code> functions, you can manually | ||
construct a router if you want to, and you can mix fs routing and manual routes | ||
if you need to.</p> | ||
<p>I like to put all my routes under <code>app/routes</code>:</p> | ||
<pre><code class="language-sh">mkdir app/routes | ||
</code></pre> | ||
<p>We can then put handlers under there and run a script to generate a module for | ||
these routes, so that the whole app can be statically checked and deployed from | ||
a single entrypoint.</p> | ||
<h3>Root URL handler</h3> | ||
<p>Let's create a handler for <code>/</code>...</p> | ||
<p>I'm making use of the response helpers, so first add that package...</p> | ||
<pre><code class="language-sh">deno add @http/response | ||
</code></pre> | ||
<p>Create <code>app/routes/index.ts</code>:</p> | ||
<pre><code class="language-ts">import { ok } from "@http/response/ok"; | ||
|
||
export function GET() { | ||
return ok("Hello"); | ||
} | ||
</code></pre> | ||
<p>Again, these helpers are completely optional, you can just use <code>new Response()</code> | ||
if you prefer.</p> | ||
<h3>Generate the router module</h3> | ||
<p>Next we'll want to generate the routes module for this:</p> | ||
<pre><code class="language-sh">deno add @http/generate | ||
</code></pre> | ||
<p>and create a script at <code>scripts/gen.ts</code>:</p> | ||
<pre><code class="language-ts">#!/usr/bin/env -S deno run --allow-ffi --allow-read=. --allow-write=. --allow-net=jsr.io | ||
|
||
import { generateRoutesModule } from "@http/generate/generate-routes-module"; | ||
|
||
function generateRoutes() { | ||
console.debug("\nGenerating routes"); | ||
|
||
return generateRoutesModule({ | ||
fileRootUrl: import.meta.resolve("../app/routes"), | ||
moduleOutUrl: import.meta.resolve("../app/routes.ts"), | ||
moduleImports: "static", | ||
verbose: true, | ||
}); | ||
} | ||
|
||
export default generateRoutes; | ||
|
||
if (import.meta.main) { | ||
await generateRoutes(); | ||
} | ||
</code></pre> | ||
<p>and add a task into your <code>deno.json</code>:</p> | ||
<pre><code class="language-json">{ | ||
"tasks": { | ||
"gen": "./scripts/gen.ts" | ||
} | ||
} | ||
</code></pre> | ||
<p>and run it:</p> | ||
<pre><code class="language-sh">deno task gen | ||
</code></pre> | ||
<p>This should have created a file at <code>app/routes.ts</code>, take a look at this in your | ||
editor.</p> | ||
<p>You'll notice it imports some packages we haven't yet added...</p> | ||
<pre><code class="language-sh">deno add @http/route | ||
</code></pre> | ||
<p>The default export of this module is a simple Request -> Response handler for | ||
all the routes in your filesystem.</p> | ||
<p>Try switching <code>moduleImports</code> to <code>"dynamic"</code> and see what is generated in | ||
<code>app/routes.ts</code> now, I'll let you work out what it's doing.</p> | ||
<p>Take a look at the | ||
<a href="https://jsr.io/@http/generate/doc/generate-routes-module/~/generateRoutesModule">generateRoutesModule()</a> | ||
function for more details along with the possible options you can supply.</p> | ||
<p>Also, take a look at | ||
<a href="https://jsr.io/@http/discovery/doc/discover-routes/~/discoverRoutes">discoverRoutes()</a>, | ||
which is what the generator uses under the covers to discover routes in the | ||
filesystem.</p> | ||
<h3>The main application handler</h3> | ||
<p>Quite often you'll want to be able to serve up things independently of the | ||
filesystem based routes (static files for example), and you may want to add | ||
common behaviour (known as middleware in other routers).</p> | ||
<p>You may also want to create multiple entry points for various purposes: | ||
development, production, for deno deploy, for cloudflare, etc.</p> | ||
<p>I like to create a single main handler that can then be imported into the the | ||
different entry points.</p> | ||
<p>And in this example, we'll add the ability to serve up static files.</p> | ||
<pre><code class="language-sh">mkdir app/static | ||
deno add @http/route-deno | ||
</code></pre> | ||
<p>(<code>@http/route-deno</code> contains the Deno specific router function <code>staticRoute</code>)</p> | ||
<p>Create <code>app/handler.ts</code>:</p> | ||
<pre><code class="language-ts">import routes from "./routes.ts"; | ||
import { handle } from "@http/route/handle"; | ||
import { staticRoute } from "@http/route-deno/static-route"; | ||
|
||
export default handle([ | ||
routes, | ||
staticRoute("/", import.meta.resolve("./static")), | ||
]); | ||
</code></pre> | ||
<p>This creates and exports a complete Request -> Response handler for our app, | ||
serving the filesystem based routes first, and then fallback to static files, | ||
and eventually falling back to a default <code>Not Found</code> response.</p> | ||
<p>This <code>handler.ts</code> is module is where I'd add patterns that are too complex for | ||
filesystem routing.</p> | ||
<h3>The production entry point</h3> | ||
<p>For this example I won't assume any particular production environment.</p> | ||
<p>Create a <code>app/main.ts</code>:</p> | ||
<pre><code class="language-ts">#!/usr/bin/env -S deno run --allow-net --allow-read=. | ||
|
||
import handler from "./handler.ts"; | ||
|
||
await Deno.serve(handler).finished; | ||
</code></pre> | ||
<p>or you could use the new <code>deno serve</code> convention instead, and it's as simple as:</p> | ||
<pre><code class="language-ts">#!/usr/bin/env -S deno serve --allow-net --allow-read=. | ||
|
||
import handler from "./handler.ts"; | ||
|
||
export default { | ||
fetch: handler, | ||
}; | ||
</code></pre> | ||
<p>and add a task to your <code>deno.json</code>:</p> | ||
<pre><code class="language-json">{ | ||
"tasks": { | ||
... | ||
"start:prod": "./app/main.ts" | ||
}, | ||
} | ||
</code></pre> | ||
<p>You now have a runnable app:</p> | ||
<pre><code class="language-sh">deno task start:prod | ||
</code></pre> | ||
<h3>The development entry point</h3> | ||
<p>During development we may want to do some additional or alternative | ||
configuration, so I like to create a separate entry point for that, and use a | ||
helper function to add logging, read local TLS certs etc.</p> | ||
<p>We're also going to rebuild our routes module automatically on restart, so we | ||
also need to be able to deal with an initially non-existing or modified routes | ||
module.</p> | ||
<pre><code class="language-sh">deno add @http/host-deno-local | ||
</code></pre> | ||
<p>Create a <code>app/dev.ts</code>:</p> | ||
<pre><code class="language-ts">!/usr/bin/env -S deno run --allow-ffi --allow-read=. --allow-write=. --allow-net --watch | ||
|
||
import generateRoutes from "../scripts/gen.ts"; | ||
import init from "@http/host-deno-local/init"; | ||
|
||
await generateRoutes(); | ||
|
||
// This allows loading of a new or modified routes.ts module | ||
const handler = lazy(import.meta.resolve("./handler.ts")); | ||
|
||
await Deno.serve(await init(handler)).finished; | ||
</code></pre> | ||
<p>and add a task to your <code>deno.json</code>:</p> | ||
<pre><code class="language-json">{ | ||
"tasks": { | ||
... | ||
"start": "./app/dev.ts" | ||
}, | ||
} | ||
</code></pre> | ||
<p>You now have a runnable dev server:</p> | ||
<pre><code class="language-sh">deno task start | ||
</code></pre> | ||
<p>BTW, you can name these entry points and tasks whatever you want, so | ||
<code>deno task dev</code> if you prefer.</p> | ||
<h3>URL Patterns</h3> | ||
<p>You can use | ||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/URLPattern">URLPattern</a> | ||
conventions in path names to match parameters, for example:</p> | ||
<p>Create <code>routes/hello-:name.ts</code>:</p> | ||
<pre><code class="language-ts">import { ok } from "@http/response/ok"; | ||
|
||
export function GET(_req: Request, match: URLPatternResult) { | ||
return ok(`Hello ${match.pathname.groups.name!}`); | ||
} | ||
</code></pre> | ||
<p>NOTE: The <code>URLPatternResult</code> pattern will be added by the <code>byPattern</code> function | ||
that wraps this handler inthe generated router.</p> | ||
<p><em>"Hang on, you can't use <code>:</code> in a file name!"</em> - I hear the Windows user scream.</p> | ||
<p>Ok, so this syntax is fine if you are on Linux/Mac etc, but Windows is a bit | ||
picky about restricted characters in filenames. So to support those users you'll | ||
probably want some kind of alternative syntax, and something to map that syntax | ||
to a valid <code>URLPattern</code>.</p> | ||
<h3>Path Mappers</h3> | ||
<p>I don't want to enforce any particular syntax, so you can provide your own path | ||
mapper to the route discovery/generator. And we provide a Fresh-like syntax | ||
mapper out of the box.</p> | ||
<p>Open your <code>routes/gen.ts</code> file again and add a new option:</p> | ||
<pre><code class="language-ts">return generateRoutesModule({ | ||
... | ||
pathMapper: "@http/discovery/fresh-path-mapper" | ||
}); | ||
</code></pre> | ||
<p>and add the import mapping for it:</p> | ||
<pre><code class="language-sh">deno add @http/discovery | ||
</code></pre> | ||
<p>You can now rename (or create) the route above as <code>routes/hello-[name].ts</code>.</p> | ||
<p>Re-start your dev app, or run <code>deno task gen</code>, and take a look at the newly | ||
generated <code>routes.ts</code> module, to see the mapping from the <code>URLPattern</code> syntax to | ||
your handler file.</p> | ||
<p>And hit <a href="http://localhost/hello-bob">http://localhost/hello-bob</a> in your browser, to see it in action.</p> | ||
<h3>Adding JSX support</h3> | ||
<p>This is completely optional, you can use whatever templating system you want, | ||
but I actually like JSX.</p> | ||
<p>This will give you server-side JSX streaming capability.</p> | ||
<p>NOTE: This is not React, or Preact, there are no hooks or other React like | ||
conventions, this is pure JSX to HTML serialization. JSX properties translate | ||
exactly to HTML attributes.</p> | ||
<pre><code class="language-sh">deno add @http/jsx-stream | ||
</code></pre> | ||
<p>Edit your <code>deno.json</code> to enable JSX compilation...</p> | ||
<pre><code class="language-json">{ | ||
"compilerOptions": { | ||
"jsx": "react-jsx", | ||
"jsxImportSource": "@http/jsx-stream" | ||
} | ||
} | ||
</code></pre> | ||
<p>Create <code>routes/hello-[name].tsx</code> (replacing <code>routes/hello-[name].ts</code>):</p> | ||
<pre><code class="language-tsx">import { html } from "@http/response/html"; | ||
import { prependDocType } from "@http/response/prepend-doctype"; | ||
import { renderBody } from "@http/jsx-stream/serialize"; | ||
|
||
export function GET(_req: Request, match: URLPatternResult) { | ||
return html( | ||
prependDocType( | ||
renderBody(<Hello name={match.pathname.groups.name!} />), | ||
), | ||
); | ||
} | ||
|
||
function Hello({ name }: { name: string }) { | ||
return ( | ||
<html> | ||
<body> | ||
<h1>Hello {name}</h1> | ||
</body> | ||
</html> | ||
); | ||
} | ||
</code></pre> | ||
<p>NOTE: The <code>renderBody</code> will serialize your JSX verbatim as a <code>ReadableStream</code> of | ||
HTML. So the <code>prependDocType</code> function is required to tag <code><!DOCTYPE html></code> to | ||
the start of your Response body.</p> | ||
<h3>Now what?</h3> | ||
<p>Go and start tinkering.</p> | ||
<p>And/or take a look at my <a href="https://github.com/jollytoad/home">personal homepage</a>, | ||
which is built using <code>@http</code> functions, and runs on Deno Deploy. It may vary a | ||
little from the conventions I describe here, but if you find the <code>dev.ts</code> & | ||
<code>main.ts</code> entrypoints you should be able to follow every path in the entire app | ||
from there.</p><script src="/prism.js" async></script></main><footer><div class="src"><a href="https://github.com/jollytoad/home/blob/main/routes/blog/http_getting_started.md" target="_blank">View source on GitHub</a></div><div>©️ 2024 Mark Gibson</div></footer><script src="/ready.js" defer></script></body></html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,11 +4,11 @@ | |
"cron" | ||
], | ||
"tasks": { | ||
"build": "deno run --allow-all scripts/build.ts", | ||
"cache": "deno run --allow-all scripts/cache.ts", | ||
"build": "./scripts/build.ts", | ||
"cache": "./scripts/cache.ts", | ||
"cache:clean": "rm -rf cache", | ||
"gen": "deno run --allow-net --allow-read --allow-write --allow-env --allow-ffi scripts/gen.ts", | ||
"start": "deno run --allow-net --allow-read --allow-env --env --allow-sys --allow-run --allow-write --allow-hrtime --allow-ffi --watch dev.ts", | ||
"gen": "./scripts/gen.ts", | ||
"start": "./dev.ts", | ||
"start:prod": "deno run --allow-net --allow-env main.ts", | ||
"mkcert": "mkcert -install -key-file localhost-key.pem -cert-file localhost-cert.pem localhost", | ||
"deploy": "deno run --allow-sys --allow-net --allow-read --allow-write --allow-env jsr:@deno/deployctl deploy", | ||
|
@@ -38,16 +38,16 @@ | |
"imports": { | ||
"$store": "https://deno.land/x/[email protected]/deno_kv.ts", | ||
"@cross/env": "jsr:@cross/env@^1.0.2", | ||
"@http/discovery": "jsr:@http/discovery@^0.14.0", | ||
"@http/generate": "jsr:@http/generate@^0.14.0", | ||
"@http/host-deno-deploy": "jsr:@http/host-deno-deploy@^0.14.0", | ||
"@http/host-deno-local": "jsr:@http/host-deno-local@^0.14.0", | ||
"@http/interceptor": "jsr:@http/interceptor@^0.14.1", | ||
"@http/discovery": "jsr:@http/discovery@^0.15.0", | ||
"@http/generate": "jsr:@http/generate@^0.15.0", | ||
"@http/host-deno-deploy": "jsr:@http/host-deno-deploy@^0.15.0", | ||
"@http/host-deno-local": "jsr:@http/host-deno-local@^0.15.0", | ||
"@http/interceptor": "jsr:@http/interceptor@^0.15.0", | ||
"@http/jsx-stream": "jsr:@http/jsx-stream@^0.2.2", | ||
"@http/request": "jsr:@http/request@^0.14.0", | ||
"@http/response": "jsr:@http/response@^0.14.0", | ||
"@http/route": "jsr:@http/route@^0.14.0", | ||
"@http/route-deno": "jsr:@http/route-deno@^0.14.0", | ||
"@http/request": "jsr:@http/request@^0.15.0", | ||
"@http/response": "jsr:@http/response@^0.15.0", | ||
"@http/route": "jsr:@http/route@^0.15.0", | ||
"@http/route-deno": "jsr:@http/route-deno@^0.15.0", | ||
"@std/async": "jsr:@std/async@^0.224.2", | ||
"@std/collections": "jsr:@std/collections@^0.224.2", | ||
"@std/fs": "jsr:@std/fs@^0.229.2", | ||
|
Oops, something went wrong.