File uploads

Accept multipart uploads, validate with z.file(), save to disk, return a typed JSON response.

Prerequisites

  • --allow-read / --allow-write for the upload directory
  • Client sends multipart/form-data (not JSON)

Server

import { HttpServer, z } from "jsr:@webtools/expressapi";

await Deno.mkdir("./uploads", { recursive: true });

export const server = new HttpServer()
  .post("/uploads", async (req, res) => {
    const { file, title } = req.body;
    const safeName = file.name.replace(/[^a-zA-Z0-9._-]/g, "_");
    const path = `./uploads/${crypto.randomUUID()}-${safeName}`;

    const bytes = new Uint8Array(await file.arrayBuffer());
    await Deno.writeFile(path, bytes);

    return res.status(201).json({
      id: path,
      name: file.name,
      size: file.size,
      type: file.type,
      title,
    });
  }, [], {
    body: z.object({
      file: z.file()
        .maxSize(5 * 1024 * 1024)
        .type(["image/", "application/pdf"]),
      title: z.string().min(1).max(100),
    }),
  })
  .get("/uploads/:name", async (req, res) => {
    return await res.sendFile(`./uploads/${req.params.name}`);
  });

export type AppRouter = typeof server;

if (import.meta.main) server.listen(5050);

Client (FormData)

const form = new FormData();
form.append("title", "Cover");
form.append("file", fileInput.files[0]);

const res = await fetch("http://localhost:5050/uploads", {
  method: "POST",
  body: form,
});
const json = await res.json();

HttpClient is ideal for JSON APIs. For browser file picks, raw FormData + fetch is usually simpler; keep the server response typed via res.json() for other typed callers.