Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Build web applications with Deno and Preact.
Mage Server
Installation
deno add jsr:@mage/server npm:[email protected]
NB: its important we're in sync with the version of Preact that we're using or Context API won't work.
Getting started
Minimum TypeScript compiler options:
{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" } }
An example app:
import { MageApp, StatusCode } from "@mage/server"; const app = new MageApp(); app.get("/", (context) => { context.text(StatusCode.OK, "Hello, World!"); }); Deno.serve(app.build());
Run the app:
deno run --allow-all main.tsx
Middleware
APIs are composed of stacked middleware. A simple middleware looks like this:
app.get("/", async (context, next) => { console.log("Request received"); await next(); });
If you want to complete handling a request you simply don't call the next middleware:
app.get("/", (context) => { context.text(StatusCode.OK, "Hello, World!"); });
You can register middleware to execute on every route and method via the
app.use method. This is useful for middleware that should run on every
request.
app.use(async (context, next) => { console.log("Request received"); await next(); });
You can configure multiple middleware at a time:
app.get( "/", (context) => { context.text(StatusCode.OK, "One!"); }, (context) => { context.text(StatusCode.OK, "Two!"); }, (context) => { context.text(StatusCode.OK, "Three!"); }, // ... etc );
Available middleware
A collection of prebuilt middleware is available to use.
useCors |
Configure CORS request handling |
useMethodNotAllowed |
Responds with 405, ignores preflight (OPTIONS) requests |
useNotFound |
Responds with 404, ignores preflight (OPTIONS) requests |
useOptions |
Responds to preflight (OPTIONS) requests |
useSecurityHeaders |
Adds recommended security headers to the response |
useServeFiles |
Serve files from a durectory based on the wildcard on context |
useValidate |
Validate request body based on a schema |
Context
A context object is passed to each middleware.
MageRequest
The request object is available on the context as a MageRequest that provides
memoized access to the body.
This is useful because the body of a request can only be read once. If you rea the body of a request, and then try to read it again, you will get an er r. This class memoizes the body of the request so that you can read it ultiple times by middlewar
context.request.method context.request.url context.request.header("Content-Type") await context.request.text() ...
Response
The response object is available on the context.
context.response.headers.set("Content-Type", "text/plain"); context.response.headers.delete("Content-Type", "text/plain");
Response utilities
A number of utility methods are available to configure the response content.
text
Respond with text.
context.text(StatusCode.OK, "Hello, World!");
json
Respond with JSON.
context.json(StatusCode.OK, { message: "Hello, World!" });
render
Render JSX to HTML using Preact.
await context.render( StatusCode.OK, <html lang="en"> <body> <h1>Hello, World!</h1> </body> </html>, );
empty
Respond with an empty response, useful for response like 204 No Content.
context.empty(StatusCode.NoContent);
redirect
Redirect the request to another location.
context.redirect(RedirectType.Permanent, "/new-location");
rewrite
You can rewrite requests to another location. This works for local and external URLs.
NOTE: This is not optimal for local rewrites, as it will make a new request to the provided location. This is useful for proxying requests to another server.
await context.rewrite("/new-location"); await context.rewrite("https://example.com");
serveFile
Serve a file from the file system.
await context.serveFile("path/to/file");
Cookies
You can read cookies from the request.
context.cookies.get("name");
You can set and delete cookies on the response.
context.cookies.set("name", "value"); context.cookies.delete("name");
Parameters
Parameters are parsed from the URL and placed on the context.
// /user/:id/post/:postId -> /user/1/post/2 context.params.id; // 1 context.params.postId; // 2
Wildcards
Wildcards are parsed from the URL and placed on the context.
// /public/* -> /public/one/two/three context.wildcard; // one/two/three
Validation
You can validate the request based on a schema using the useValidate()
middleware based on a Standard Schema
schema.
You can define where the validator should source the data from.
json- The request bodyform- The request form dataparams- The route paramssearch-params- The URL search parameters
app.use(useValidate("json", schema));
When this middleware is used, the request body will be validated and the result
will be placed on context. You can access it using:
context.valid("json", schema);
Web sockets
You can upgrade a request to a web socket connection. If the request is not a WebSocket request it will send a 501 Not Implemented response and no WebSocket will be created.
app.get("/ws", async (context) => { context.webSocket((socket) => { socket.onmessage = (event) => { if (event.data === "ping") { socket.send("pong"); } }; }); });
Assets
You can get a cache busted path for the asset via the context.asse() method.
This will append the build id to the asset path. The useServeFiles middleware
supports serving the files and stripping the build id from the path. Files that
have been cache busted in this way will be served with a Cache-Control header
set to max-age=31536000 (1 year).
app.get("/public/*", useServeFiles({ directory: "./public" })); app.get("/", async (context) => { await context.render( StatusCode.OK, <html lang="en"> <body> <img src={context.asset("/public/image.png")} /> </body> </html>, ); });
Routing
HTTP methods
Routes can be registered for each HTTP method against a route:
app.get("/", (context) => { context.text(StatusCode.OK, "Hello, World!"); }); // ... post, delete, put, patch, options, head, all
You can also register a route for all HTTP methods:
app.all("/", (context) => { context.text(StatusCode.OK, "Hello, World!"); });
You can exclude the route and just register middleware against a HTTP method:
app.options((context) => { console.log("Custom OPTIONS handler"); });
Paths
Paths can be simple:
app.get("/one", (context) => { context.text(StatusCode.OK, "Simple path"); }); app.get("/one/two", (context) => { context.text(StatusCode.OK, "Simple path"); }); app.get("/one/two/three", (context) => { context.text(StatusCode.OK, "Simple path"); });
Parameters
Paths can contain parameters that will be available on context.params.<name>
as strings.:
app.get("/user/:id", (context) => { context.text(StatusCode.OK, `User ID: ${context.params.id}`); }); app.get("/user/:id/post/:postId", (context) => { context.text( StatusCode.OK, `User ID: ${context.params.id}, Post ID: ${context.params.postId}`, ); });
Wildcards
Paths can contain wildcards that will match any path. Wildcards must be at the end of the path.
app.get("/public/*", (context) => { context.text(StatusCode.OK, "Wildcard path"); });
The path portion captured by the wildcard is available on context.wildcard.
app.get("/public/*", (context) => { context.text(StatusCode.OK, `Wildcard path: ${context.wildcard}`); });
Wildcards are inclusive of the path its placed on. This means that the wildcard will match any path that starts with the wildcard path.
app.get("/public/*", (context) => { context.text(StatusCode.OK, "Wildcard path"); }); /** * matches: * * /public * /public/one * /public/one/two */
Running your app
To run your app, you can use the Deno.serve function:
Deno.serve(app.build());
Header utilities
Some utility methods are available to configure common complex response headers.
cacheControl
Set the Cache-Control header.
cacheControl(context, { maxAge: 60, });
contentSecurityPolicy
Set the Content-Security-Policy header.
contentSecurityPolicy(context, { directives: { defaultSrc: "'self'", scriptSrc: ["'self'", "https://example.com"], }, });
Add Package
deno add jsr:@mage/server
Import symbol
import * as server from "@mage/server";
Import directly with a jsr specifier
import * as server from "jsr:@mage/server";