Files
Upload multipart files into typed handlers, and stream downloads with sendFile.
How multipart is parsed
When Content-Type starts with multipart/form-data, ExpressAPI reads the body with request.formData() and builds a plain object via Object.fromEntries(formData). Text fields become strings; file fields become Web File instances.
Malformed bodies yield null instead of throwing. GET requests never receive a body.
Upload with validation
Use z.file() on the field that must be a File. Mix text fields and files in the same schema:
import { HttpServer, z } from "jsr:@webtools/expressapi";
const server = new HttpServer()
.post("/upload", async (req, res) => {
const { file, title } = req.body;
// file: File, title: string
const bytes = new Uint8Array(await file.arrayBuffer());
await Deno.writeFile(`./uploads/${file.name}`, bytes);
return res.status(201).json({
uploaded: true,
name: file.name,
size: file.size,
type: file.type,
title,
});
}, [], {
body: z.object({
file: z.file().maxSize(5 * 1024 * 1024).type(["image/"]),
title: z.string().min(1),
}),
});z.file() supports minSize, maxSize (bytes), and type(types) where each entry is matched with file.type.startsWith(...).
Reading a File
file.name,file.size,file.type- metadataawait file.arrayBuffer()- load into memoryfile.stream()- stream without buffering the whole file
.post("/avatar", async (req, res) => {
const { avatar } = req.body;
const stream = avatar.stream();
const out = await Deno.open(`./avatars/${avatar.name}`, {
create: true,
write: true,
truncate: true,
});
await stream.pipeTo(out.writable);
return res.json({ ok: true, name: avatar.name });
}, [], {
body: z.object({ avatar: z.file() }),
})Null body
If parsing fails, req.body is null. A schema will then fail validation and return 400 before your handler. Without a schema, guard explicitly:
.post("/raw-upload", (req, res) => {
if (req.body == null) {
return res.status(400).json({ error: "Invalid multipart body" });
}
return res.json({ keys: Object.keys(req.body as object) });
})Serving files
await res.sendFile(path) opens the path with Deno, sets Content-Type from the extension (via @std/media-types), sets Content-Length, and streams the readable. Missing paths throw - catch them with onError.
.get("/files/:name", async (req, res) => {
return await res.sendFile(`./uploads/${req.params.name}`);
})
// In-memory alternative:
.get("/report.pdf", async (_req, res) => {
const bytes = await Deno.readFile("./report.pdf");
return res.type("pdf").size(bytes.byteLength).send(bytes);
})res.json() is what types the client. sendFile / send return a plain Response, so they contribute no response shape to HttpClient.
End-to-end upload recipe: File uploads. Body parsing overview: Request & response.