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 - metadata
  • await file.arrayBuffer() - load into memory
  • file.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.