@http functions is not a framework, it's a library of functions that work well together, but also with other frameworks (that use standard web APIs).
At present there is no init
command to create a template app.
I feel the main benefit of using @http
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.
So with this in mind, this guide is a like a
Linux From Scratch but for a web app, using
this library of @http
functions.
I'm assuming you have Deno installed and your favourite IDE ready to work with it.
- Create the basic project structure
- Add a request handler for
/
- Generate a router from filesystem based routing structure
- Add static file routing
- Add production and development entry points
- Add a route with a pattern
- Add a path syntax mapper
- Add JSX support
First create a new folder for your project, and cd
into it.
I put all utility scripts into scripts
, and the app itself into app
, this is
just a convention I like, you can structure it however you want.
mkdir app scripts
This is completely optional when using @http
functions, you can manually
construct a router if you want to, and you can mix fs routing and manual routes
if you need to.
I like to put all my routes under app/routes
:
mkdir app/routes
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.
Let's create a handler for /
...
I'm making use of the response helpers, so first add that package...
deno add @http/response
Create app/routes/index.ts
:
import { ok } from "@http/response/ok";
export function GET() {
return ok("Hello");
}
Again, these helpers are completely optional, you can just use new Response()
if you prefer.
Next we'll want to generate the routes module for this:
deno add @http/generate
and create a script at scripts/gen.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();
}
and add a task into your deno.json
:
{
"tasks": {
"gen": "./scripts/gen.ts"
}
}
and run it:
deno task gen
This should have created a file at app/routes.ts
, take a look at this in your
editor.
You'll notice it imports some packages we haven't yet added...
deno add @http/route
The default export of this module is a simple Request -> Response handler for all the routes in your filesystem.
Try switching moduleImports
to "dynamic"
and see what is generated in
app/routes.ts
now, I'll let you work out what it's doing.
Take a look at the generateRoutesModule() function for more details along with the possible options you can supply.
Also, take a look at discoverRoutes(), which is what the generator uses under the covers to discover routes in the filesystem.
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).
You may also want to create multiple entry points for various purposes: development, production, for deno deploy, for cloudflare, etc.
I like to create a single main handler that can then be imported into the the different entry points.
And in this example, we'll add the ability to serve up static files.
mkdir app/static
Create app/handler.ts
:
import routes from "./routes.ts";
import { handle } from "@http/route/handle";
import { staticRoute } from "@http/route/static-route";
export default handle([
routes,
staticRoute("/", import.meta.resolve("./static")),
]);
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 Not Found
response.
This handler.ts
is module is where I'd add patterns that are too complex for
filesystem routing.
For this example I won't assume any particular production environment.
Create a app/main.ts
:
#!/usr/bin/env -S deno run --allow-net --allow-read=.
import handler from "./handler.ts";
await Deno.serve(handler).finished;
or you could use the new deno serve
convention instead, and it's as simple as:
#!/usr/bin/env -S deno serve --allow-net --allow-read=.
import handler from "./handler.ts";
export default {
fetch: handler,
};
and add a task to your deno.json
:
{
"tasks": {
...
"start:prod": "./app/main.ts"
},
}
You now have a runnable app:
deno task start:prod
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.
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.
deno add @http/host-deno-local
Create a app/dev.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";
import { lazy } from "@http/route/lazy";
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;
and add a task to your deno.json
:
{
"tasks": {
...
"start": "./app/dev.ts"
},
}
You now have a runnable dev server:
deno task start
BTW, you can name these entry points and tasks whatever you want, so
deno task dev
if you prefer.
You can use URLPattern conventions in path names to match parameters, for example:
Create routes/hello-:name.ts
:
import { ok } from "@http/response/ok";
export function GET(_req: Request, match: URLPatternResult) {
return ok(`Hello ${match.pathname.groups.name!}`);
}
NOTE: The URLPatternResult
pattern will be added by the byPattern
function
that wraps this handler inthe generated router.
"Hang on, you can't use :
in a file name!" - I hear the Windows user scream.
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 URLPattern
.
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.
Open your routes/gen.ts
file again and add a new option:
return generateRoutesModule({
...
pathMapper: "@http/discovery/fresh-path-mapper"
});
and add the import mapping for it:
deno add @http/discovery
You can now rename (or create) the route above as routes/hello-[name].ts
.
Re-start your dev app, or run deno task gen
, and take a look at the newly
generated routes.ts
module, to see the mapping from the URLPattern
syntax to
your handler file.
And hit http://localhost/hello-bob in your browser, to see it in action.
This is completely optional, you can use whatever templating system you want, but I actually like JSX.
This will give you server-side JSX streaming capability.
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.
deno add @http/jsx-stream
Edit your deno.json
to enable JSX compilation...
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@http/jsx-stream"
}
}
Create routes/hello-[name].tsx
(replacing routes/hello-[name].ts
):
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>
);
}
NOTE: The renderBody
will serialize your JSX verbatim as a ReadableStream
of
HTML. So the prependDocType
function is required to tag <!DOCTYPE html>
to
the start of your Response body.
Go and start tinkering.
And/or take a look at my personal homepage,
which is built using @http
functions, and runs on Deno Deploy. It may vary a
little from the conventions I describe here, but if you find the dev.ts
&
main.ts
entrypoints you should be able to follow every path in the entire app
from there.