- 0.6.2Latest
- 0.6.0
- 0.5.1
- 0.5.0
- 0.4.0
- 0.3.2
- 0.3.1
- 0.3.0
- 0.2.3
- 0.2.2
- 0.2.1
- 0.2.0
- 0.2.0-alpha.15
- 0.2.0-alpha.14
- 0.2.0-alpha.13
- 0.2.0-alpha.12
- 0.2.0-alpha.11
- 0.2.0-alpha.10
- 0.2.0-alpha.9
- 0.2.0.alpha-9
- 0.2.0-alpha.8
- 0.2.0-alpha.7
- 0.2.0-alpha.6
- 0.2.0-alpha.5
- 0.2.0-alpha.4
- 0.2.0-alpha.3
- 0.2.0-alpha.2
- 0.2.0-alpha.1
- 0.2.0-alpha
- 0.1.0
- 0.0.24
- 0.0.23
- 0.0.22
- 0.0.21
- 0.0.20
- 0.0.19
- 0.0.18
- 0.0.17
- 0.0.16
- 0.0.15
- 0.0.14
- 0.0.13
- 0.0.12
- 0.0.11
- 0.0.10
- 0.0.9
- 0.0.8
- 0.0.7
- 0.0.6
- 0.0.5
- 0.0.4
- 0.0.3
- 0.0.2
- 0.0.1
- 0.0.0
Cav
An experimental server framework for Deno.
NOTE: This project is currently being rebuilt from the ground up. See here for the latest release. All published versions are prototypes and should NOT be used in production.
Features:
- Declarative request routing
- Flexible body parsing
- No middleware, yes custom context
- Signed cookies
- Web socket support
- Client function (wraps fetch)
- End-to-end type safety
- Runtime TypeScript bundling (maybe)
The rest of this readme is a temporary home for the rough draft of the getting started guide.
Setup
Cav builds on standard Deno concepts. If you havenāt already, read through the manual to get acquainted.
Further, the Deno Deploy documentation is helpful for learning more about handling HTTP with Denoās standard library.
Routing requests
Handlers
Denoās http module exports the following handler type:
// https://deno.land/std/http/server.ts
export interface ConnInfo {
readonly localAddr: Deno.Addr;
readonly remoteAddr: Deno.Addr;
}
export type Handler = (
request: Request,
connInfo: ConnInfo,
) => Response | Promise<Response>;
Cavās Router accepts handlers that implement this standard type while providing a few extra properties on the second argument:
// https://deno.land/x/cav/router.ts
// ConnInfo comes from Deno's http module
import type { ConnInfo } from "./deps.ts";
export interface Context extends ConnInfo {
readonly url: URL;
readonly path: string;
readonly param: { readonly [x: string]: string };
}
export type Handler = (
req: Request,
context: Context,
) => Response | Promise<Response>;
The url
property is a URL instance of the full request url. The path
property is the unrouted portion of the request path, and the param
property
is for path parameters captured during routing. The first Router to encounter a
request will set the path
equal to the full request path, and the param
object will be empty. As the request is routed through the handler tree, these
properties will be updated; newly captured path parameters will merge with
previously captured parameters, and the path will shrink from the left hand side
as path segments are āconsumed.ā
Note: Because of the way the Handler type is defined, Routers also accept standard Deno handlers, such as those in the Deno Deploy examples gallery.
Declaring routers
To create a standard Deno handler for routing requests between Cav Handlers
(using the request path), use the router
factory function:
// https://deno.land/x/cav/router.ts
import type { ConnInfo } from "./deps.ts";
export interface RouterShape {
// The keys are the route path
[x: string]: Handler | Handler[] | null;
}
export type Router<Shape extends RouterShape> = (
& Shape
& ((req: Request, connInfo: ConnInfo) => Promise<Response>)
);
export function router<Shape extends RouterShape>(shape: Shape): Router<Shape> {
// ...
}
Because the created routers are standard Deno handlers, you can serve them with the standard library:
#!/usr/bin/env deno run --allow-net
import { serve } from "https://deno.land/std/http/mod.ts";
import { serveFile } from "https://deno.land/std/http/file_server.ts";
import { router } from "https://deno.land/x/cav/mod.ts";
const app = router({
"hello.jpg": (req, context) => serveFile(req, "assets/hello.jpg"),
});
serve(app, {
onError: err => {
if (err instanceof Deno.errors.NotFound) {
return new Response("404 not found", { status: 404 });
}
console.error(err);
return new Response("500 internal server error", { status: 500 });
},
});
In the example above, a request to either localhost:8000/hello
or
localhost:8000/goodbye
will return the appropriate response and any other
request path will result in a 404 not found
error being returned to the
client.
Route paths
The router path syntax is partially inspired by the URLPattern syntax, but only basic path parameters are supported:
const app = router({
":example": (_, context) => new Response(`Example: ${context.param.example}`),
":a/b/:c": (_, context) => {
return new Response(`A: ${context.param.a}, C: ${context.param.c}`);
},
});
A request with the path /a/b/c
passed into the router above will return a
response with the text A: a, C: c
, and if the request path was /example
, it
would return Example: example
.
Paths can end in a *
segment, which allows them to match partially with a
request. The unmatched portion of the path is forwarded to the next handler on
the context
object:
const app = router({
"a/:b/*": (_, context) => {
return new Response(`B: ${context.param.b}, Path: ${context.path}`);
},
});
A request to the router above with the pathname /a/b/c/d/
will result in the
text B: b, Path: c/d/index
being sent back. (More on the index
part later.)
The wildcard doesnāt require remaining path segments to be present in order for
the route to match with a request. If a request with the path /a/b
was sent to
the router above, a response with the text B: b, Path:
will be sent back.
(The context.path
is an empty string in this case.)
When a wildcard route conflicts with a non-wildcard route, the non-wildcard
route will match first. In the following router, a request with the path /a/b
will return the text hello world
:
const app = router({
"a/b": () => new Response("hello world"),
"a/b/*": () => new Response("goodbye world"),
});
Hereās some other router path rules:
- Route paths canāt contain leading, trailing, or duplicate slashes
- The
..
and.
path segments canāt be used (theyād never match) - Wildcards can only show up at the end of a path as the last segment, and they
must appear alone, i.e.
*hello
is an invalid path segment - Path parameters can have any name, as long as their path segment starts with
a
:
- Paths donāt match against the whole URL. Only the pathname is used during routing
404s
To tell the router to continue matching from inside a handler, throw an instance
of Deno.errors.NotFound
. In the example below, a request to /a
will return a
plaintext b
response:
const app = router({
"a": () => { throw new Deno.errors.NotFound() },
"*": () => new Response("b"),
});
Note: If the request has a body and the body is consumed before a NotFound error is thrown, path matching will not continue and the error will be rethrown inside the router.
Handler arrays
When multiple handlers need to be registered at the same path, use an array of handlers. Each handler is attempted until one of them returns a response instead of throwing a NotFound error:
const app = router({
"api/hello": () => new Response("hello world"),
"*": [
(req, context) => serveFile(req, stdPath.join("assets1", context.path)),
(req, context) => serveFile(req, stdPath.join("assets2", context.path)),
],
});
Router composition
Constructed router handlers have the same properties as the RouterShapes used to create them, allowing routers to build on one another as if they were regular JavaScript objects:
const helloApp = router({
"hello": () => new Response("hello world"),
});
const goodbyeApp = router({
"goodbye": () => new Response("goodbye world"),
});
const app = router({
...helloApp,
...goodbyeApp,
});
null
is allowed as a handler value, letting you turn off certain routes when
composing routers:
const v1 = router({
"foo": () => new Response("hello world"),
"bar": () => new Response("goodbye world"),
});
const v2 = router({
...v1,
"foo": null,
});
Further, router nesting is a natural consequence of them being Deno handlers.
Below, a request to /foo/bar
will return a plaintext baz
response:
const app = router({
"foo/*": router({
"bar": () => new Response("baz"),
}),
});
Indexes
When thereās a trailing slash on a requested path, an index
segment will be
automatically appended during routing. The following will return a plaintext
hello world
when the request path is /blog/
:
const app = router({
"blog/*": router({
"index": () => new Response("hello world"),
}),
});
Routers can also have an empty route (""
), allowing the trailing slash to be
omitted. This time, hello world
is returned when the request path is /blog
:
const app = router({
"blog/*": router({
"": () => new Response("hello world"),
}),
});
The root path is special. Because the leading slash is always inferred when a
request is made, both the index
and the empty route ""
will be checked iff
the unrouted request path is /
. A request to the root path in the following
app will return hello world
:
const app = router({
"": () => new Response("hello world"),
});
Likewise, this app will do the same thing:
const app = router({
"index": () => new Response("hello world"),
});
The index
route is matched before the empty route in these cases. The
following will send back a hello
(not goodbye
) when the request path is /
:
const app = router({
"index": () => new Response("hello"),
"": () => new Response("goodbye"),
});
Note that above router wonāt behave the same if itās nested inside a different
router and thereās no trailing slash on the request path. The following router
will respond with goodbye
if the request path is /nested
:
const app = router({
"nested/*": router({
"index": () => new Response("hello"),
"": () => new Response("goodbye"),
}),
});
Tip: Donāt use both index
routes and empty routes in your application. Pick
one and stick with it. If you like trailing slashes on your URLs, use index
,
and if not, use an empty route.
To be continued