Request & Response
Mage wraps the standard Web API Request and Response objects with MageRequest
and MageResponse. These wrappers solve specific problems with the standard
APIs while maintaining full compatibility.
Quick Start
Access the request and response through the context object:
import { MageApp } from "@mage/app";
const app = new MageApp();
app.post("/users", async (c) => {
// Access request data
const body = await c.req.json();
const contentType = c.req.header("content-type");
const id = c.req.searchParam("id");
// Set response headers
c.res.headers.set("X-User-Id", "123");
return c.json({ created: true });
});
Deno.serve(app.handler);
MageRequest
MageRequest wraps the standard Request object to solve a fundamental
limitation: request bodies can only be read once. MageRequest memoizes body
parsing so multiple middleware can read the same body.
Body Parsing
MageRequest provides memoized methods for parsing the request body in different formats:
app.post("/api/data", async (c) => {
// Parse as JSON
const data = await c.req.json();
// Parse as text
const text = await c.req.text();
// Parse as FormData
const formData = await c.req.formData();
// Parse as ArrayBuffer
const buffer = await c.req.arrayBuffer();
// Parse as Blob
const blob = await c.req.blob();
return c.json({ received: true });
});
All parsing methods are memoized. The first call parses the body, subsequent calls return the cached value:
app.post("/users", async (c) => {
const body1 = await c.req.json(); // Parses body
const body2 = await c.req.json(); // Returns memoized result
const body3 = await c.req.json(); // Returns memoized result
console.log(body1 === body2); // true - same object
});
Accessing Request Data
MageRequest provides convenient access to common request properties:
app.get("/users", (c) => {
// HTTP method
const method = c.req.method; // "GET"
// URL object
const url = c.req.url; // URL instance
const pathname = url.pathname; // "/users"
const hostname = url.hostname; // "example.com"
// Raw Request object
const raw = c.req.raw; // Standard Request
return c.json({ method, pathname });
});
Headers
Access request headers using the header() method:
app.post("/users", async (c) => {
// Get single header
const contentType = c.req.header("content-type");
const authorization = c.req.header("authorization");
// Headers are case-insensitive
const accept1 = c.req.header("Accept");
const accept2 = c.req.header("accept");
console.log(accept1 === accept2); // true
// Returns null if header not present
const custom = c.req.header("x-custom");
if (custom === null) {
console.log("Header not present");
}
return c.json({ contentType });
});
URL Parameters
Access URL search parameters using the searchParam() method:
app.get("/search", (c) => {
// GET /search?q=mage&limit=10
// Get single parameter
const query = c.req.searchParam("q"); // "mage"
const limit = c.req.searchParam("limit"); // "10"
// Returns null if parameter not present
const page = c.req.searchParam("page"); // null
// Access all parameters via URL object
const allParams = c.req.url.searchParams;
const tags = allParams.getAll("tag"); // Array of all "tag" values
return c.json({ query, limit });
});
Route Parameters
Access route parameters matched by the router:
app.get("/users/:id", (c) => {
// GET /users/123
// Access single parameter
const userId = c.req.params.id; // "123"
return c.json({ userId });
});
app.get("/users/:userId/posts/:postId", (c) => {
// GET /users/123/posts/456
// Access multiple parameters
const userId = c.req.params.userId; // "123"
const postId = c.req.params.postId; // "456"
return c.json({ userId, postId });
});
Parameters are always strings. Parse them if you need numbers or other types:
app.get("/users/:id", (c) => {
const id = parseInt(c.req.params.id, 10);
if (isNaN(id)) {
return c.text("Invalid ID", 400);
}
return c.json({ id });
});
Wildcard Paths
Access wildcard path segments using the wildcard property:
app.get("/files/*", (c) => {
// GET /files/images/photo.jpg
const path = c.req.wildcard; // "images/photo.jpg"
return c.json({ path });
});
app.get("/api/v1/*", (c) => {
// GET /api/v1/users/123/posts
const resource = c.req.wildcard; // "users/123/posts"
return c.json({ resource });
});
The wildcard property is undefined if the route doesn't use a wildcard
pattern.
MageRequest API Reference
Properties
| Property | Type | Description |
|---|---|---|
raw |
Request |
Underlying Web API Request object |
url |
URL |
Parsed URL object |
method |
string |
HTTP method (GET, POST, etc.) |
params |
{ [key: string]: string } |
Route parameters matched by router |
wildcard |
string | undefined |
Wildcard path segment (if route uses *) |
Methods
| Method | Returns | Description |
|---|---|---|
json() |
Promise<unknown> |
Parse body as JSON (memoized) |
text() |
Promise<string> |
Parse body as text (memoized) |
formData() |
Promise<FormData> |
Parse body as FormData (memoized) |
arrayBuffer() |
Promise<ArrayBuffer> |
Parse body as ArrayBuffer (memoized) |
blob() |
Promise<Blob> |
Parse body as Blob (memoized) |
header(name) |
string | null |
Get request header value |
searchParam(name) |
string | null |
Get URL search parameter value |
MageResponse
MageResponse delays Response object creation until the end of the middleware
chain. This allows middleware to modify headers, body, and status even after
handlers have set them. The final Response is created once at the end.
Setting Response Body
Set the response body using context helper methods or directly:
app.get("/users", (c) => {
// Using helper methods (recommended)
return c.json({ users: [] });
return c.text("Hello");
return c.html("<h1>Hello</h1>");
// Setting body directly
c.res.setBody(JSON.stringify({ users: [] }));
c.res.setStatus(200);
return; // Returns response set on context
});
The response body can be any valid BodyInit type:
app.get("/data", (c) => {
// String
c.res.setBody("Hello");
// ArrayBuffer
c.res.setBody(new ArrayBuffer(8));
// Blob
c.res.setBody(new Blob(["Hello"]));
// FormData
const formData = new FormData();
formData.append("key", "value");
c.res.setBody(formData);
// ReadableStream
const stream = new ReadableStream({
start(controller) {
controller.enqueue("Hello");
controller.close();
},
});
c.res.setBody(stream);
return;
});
Setting Status Code
Set the response status code and optional status text:
app.post("/users", (c) => {
// Using helper methods
return c.json({ created: true }, 201);
return c.text("Not Found", 404);
// Setting status directly
c.res.setStatus(201, "Created");
c.res.setBody(JSON.stringify({ created: true }));
return;
});
Modifying Headers
MageResponse maintains mutable headers throughout the middleware chain. You can set headers before, during, or after setting the response body:
app.use(async (c, next) => {
// Set headers before handler
c.res.headers.set("x-request-id", "123");
await next();
// Set headers after handler
c.res.headers.set("x-processing-time", "45ms");
});
app.get("/users", (c) => {
// Set headers in handler
c.res.headers.set("cache-control", "max-age=3600");
return c.json({ users: [] });
});
Headers remain mutable until the response is finalized:
app.use(async (c, next) => {
c.res.headers.set("x-version", "1.0");
await next();
// Can still modify headers
c.res.headers.set("x-version", "2.0"); // Overwrites previous value
c.res.headers.append("vary", "accept-encoding");
c.res.headers.delete("x-debug");
});
External Responses
MageResponse supports external Response objects for file serving, redirects, and WebSocket upgrades:
app.get("/files/:path", (c) => {
const file = Deno.readFileSync(`./uploads/${c.req.params.path}`);
const response = new Response(file, {
headers: { "content-type": "application/octet-stream" },
});
// Set external response
c.res.setExternal(response);
// Can still add headers
c.res.headers.set("x-served-by", "mage");
return;
});
When you set an external Response, MageResponse merges headers intelligently:
app.get("/download", (c) => {
// Set headers first
c.res.headers.set("x-custom", "value");
// External Response has its own headers
const response = new Response("data", {
headers: { "content-type": "text/plain" },
});
c.res.setExternal(response);
// Final response includes both:
// - x-custom: value (from MageResponse)
// - content-type: text/plain (from external Response)
return;
});
User-set headers take precedence over external Response headers:
app.get("/override", (c) => {
// Set content-type first
c.res.headers.set("content-type", "application/json");
// External Response has different content-type
const response = new Response("data", {
headers: { "content-type": "text/plain" },
});
c.res.setExternal(response);
// Final response uses your header:
// content-type: application/json (user-set header wins)
return;
});
WebSocket Upgrade
WebSocket upgrades (status 101) are handled specially:
app.get("/ws", (c) => {
const upgrade = Deno.upgradeWebSocket(c.req.raw);
// Set the upgrade response
c.res.setExternal(upgrade.response);
// WebSocket upgrade responses are returned as-is
// They cannot be cloned or modified
return;
});
Response Finalization
MageResponse creates the final Response object automatically at the end of the
middleware chain via finalize(). You never need to call it manually.
Checking Response State
Check if a response body has been set:
app.use(async (c, next) => {
await next();
// Check if handler set a response
if (!c.res.hasBody()) {
c.res.setBody("Default response");
c.res.setStatus(200);
}
});
app.get("/maybe", (c) => {
// Might not set a response
if (Math.random() > 0.5) {
return c.text("Hello");
}
// No response set - middleware will add default
});
MageResponse API Reference
Properties
| Property | Type | Description |
|---|---|---|
body |
BodyInit | null |
Response body (or external Response body) |
status |
number |
HTTP status code (or external Response status) |
statusText |
string |
HTTP status text (or external Response statusText) |
headers |
Headers |
Mutable Headers object |
Methods
| Method | Returns | Description |
|---|---|---|
setBody(body) |
void |
Set response body (clears external Response) |
setStatus(status, text?) |
void |
Set status code and optional status text |
setExternal(response) |
void |
Set external Response (merges headers) |
hasBody() |
boolean |
Check if response body is set |
finalize() |
Response |
Create final Response object (called by MageApp) |
Notes
Request body memoization:
- All body parsing methods cache their results
- The first call parses the body and stores it
- Subsequent calls return the cached value
- Different parsing methods share the same underlying body stream
- Once you parse as JSON, you cannot parse as text (the stream is consumed)
Response mutability:
- Headers remain mutable throughout the middleware chain
- Body and status can be changed at any time before finalization
- Setting a new body clears any external Response
- External Response headers are merged, not replaced
Performance:
- MageRequest memoization adds minimal overhead (simple cache lookup)
- MageResponse creates a single Response object per request (no object churn)
- Header modifications are in-place (no copying until finalization)
External Responses:
- Used for file serving, WebSocket upgrades, and proxying
- Headers are merged intelligently (user headers take precedence)
- WebSocket upgrades (status 101) are returned as-is without modification
Related
- MageContext - The context object containing request and response
- Routing - Route patterns, parameters, and wildcards
- Middleware - Middleware patterns and execution order