close
Skip to main content
This package has been archived, and as such it is read-only.
This release was yanked — the latest version of @mage/server is 0.7.3.

@mage/server@0.14.1
Built and signed on GitHub Actions

Works with
This package works with DenoIt is unknown whether this package works with Cloudflare Workers, Node.js, Bun, Browsers
It is unknown whether this package works with Cloudflare Workers
It is unknown whether this package works with Node.js
This package works with Deno
It is unknown whether this package works with Bun
It is unknown whether this package works with Browsers
JSR Score100%
Publisheda year ago (0.14.1)

Build web applications with Deno and Preact.

Emotion logo

Mage Server

Build web applications with Deno and Preact

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 body
  • form - The request form data
  • params - The route params
  • search-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"],
  },
});
Built and signed on
GitHub Actions

Report package

Please provide a reason for reporting this package. We will review your report and take appropriate action.

Please review the JSR usage policy before submitting a report.