Runtime
At build time, Marko Run generates a router from the application's route files. For each request, the router creates a context object that flows through middleware, handlers, and templates. When using an adapter, the runtime is abstracted away. It can also be embedded in an existing server.
Context
The context is passed to middleware and handler functions as the first parameter, conventionally named ctx. In Marko templates, it is available as $global. It contains information about the current request:
| Property | Description |
|---|---|
route | A string identifying the current route's path pattern |
request | The current WHATWG Request instance |
method | HTTP method of the current request |
url | WHATWG URL instance of the current request |
params | The route's path parameters, transformed by any params validators |
search | The request's query string values, transformed by any search validators |
body | Promise for the parsed request body when the route configures a json or form option, otherwise undefined |
data | Data passed from upstream middleware and handlers via next(data) |
meta | Metadata loaded from the current route's +meta file |
platform | Additional data provided by the adapter |
parent | The calling context when the request was made with ctx.fetch, otherwise undefined |
In templates, $global.params and $global.url are serialized to the browser by default, so client-side code can read them after hydration. Other context properties can be included by setting them in ctx.serializedGlobals.
Context Methods
The context also provides helpers for producing responses.
fetch
fetch(resource: string | URL | Request, init?: RequestInit): Promise<Response>;Creates a response by making a new request to the router. This method has the same signature as native fetch. Relative URLs resolve against the current request's URL, and the new request's context has this context as its parent.
render
render<T>(template: Marko.Template<T>, input: T, init?: ResponseInit): Response;Creates a response that streams the given Marko template and sets the Content-Type header to text/html.
redirect
redirect(to: string | URL, status?: number): Response;Creates a redirect response, resolving relative paths against the current URL. The status defaults to 302 and must be one of 301, 302, 303, 307, or 308.
back
back(fallback?: string | URL, status?: number): Response;Creates a redirect response that uses the current request's Referer header, or the fallback (default /) when there is none.
Typed URLs
The runtime also provides Run.href, which builds URLs for the application's routes with type checking of the path, params, search, and hash. The path is typed against the routes the application actually serves. Renaming or moving a route turns every stale link into a compile-time error instead of a broken <a> tag.
<a href=Run.href("/projects/$projectId/members", {
params: {
projectId: 42
},
search: {
sort: "name"
},
hash: "top"
})>
Members
</a><a href=Run.href("/projects/$projectId/members", {
params: { projectId: 42 },
search: { sort: "name" },
hash: "top",
})>
Members
</a>a href=Run.href("/projects/$projectId/members", {
params: {
projectId: 42
},
search: {
sort: "name"
},
hash: "top"
}) --
Membersa href=Run.href("/projects/$projectId/members", {
params: { projectId: 42 },
search: { sort: "name" },
hash: "top",
}) --
Members| Option | Description |
|---|---|
params | Values for the path's dynamic segments. Required exactly when the path has them. Catch-all ($$) params also accept arrays, joined with /. |
search | Query string values. When the route declares a search validator, the keys are typed against it. |
hash | Fragment appended after #. |
All params, search values, and the hash are URI-encoded automatically.
The full path checking relies on generated types, so a TSConfig file must be present in the project root for Run.href to validate paths and params at compile time. The function still builds correct URLs without it.
In client builds, Marko Run rewrites statically analyzable Run.href(...) calls at compile time, so most calls cost nothing at runtime.
Embedding
When more control is needed than an adapter provides, the router can be imported directly and embedded in an existing server:
import * as router from "@marko/run/router";router.fetch
function fetch(request: Request, platform: unknown): Promise<Response | void>;This asynchronous function takes a WHATWG Request and an object containing any platform-specific data made available as ctx.platform. It returns any of
- a WHATWG
Responsegenerated from executing the matched route files undefinedif the request was not explicitly handled- a
404status response if no route matches the requested path - a
500status response if an uncaught error occurs
Express example:
import express from "express";
import * as router from "@marko/run/router";
express()
.use(async (req, res, next) => {
const request = createWHATWGRequest(req); // code to create a WHATWG Request from `req`
const response = await router.fetch(request, { req, res });
if (response) {
applyResponse(response, res); // code to apply a WHATWG Response to `res`
} else {
next();
}
})
.listen(3000);When targeting Node.js servers, the Node adapter's middleware already performs this request and response conversion for Connect-style servers like Express.
router.match
function match(method: string, pathname: string): Route | null;This synchronous function takes an HTTP method and path name. It returns an object representing the best matching route (params and meta), or null if no route matches. This is useful when other parts of a server need to know whether a route exists before creating a response.
router.invoke
function invoke(
route: Route,
request: Request,
platform: unknown,
): Promise<Response | void>;This asynchronous function takes a route object returned by router.match, along with the request and platform data. It produces a response in the same way router.fetch does. Together, match and invoke split fetch into its two steps so that other middleware can run in between:
import express from "express";
import * as router from "@marko/run/router";
express()
.use((req, res, next) => {
res.locals.match = router.match(req.method, req.path);
next();
})
// ...other middleware which can check `res.locals.match`
.use(async (req, res, next) => {
if (!res.locals.match) {
next();
return;
}
const request = createWHATWGRequest(req); // code to create a WHATWG Request from `req`
const response = await router.invoke(res.locals.match, request, { req, res });
if (response) {
applyResponse(response, res); // code to apply a WHATWG Response to `res`
} else {
next();
}
})
.listen(3000);Next Steps
Contributors
Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.