# Writing functions: Deno, imports and structure

> How to structure an Edge Function on SupaHost: the index.ts entry file, Deno.serve, npm/jsr/https imports, extra files, routing and error handling.

Source: https://supahost.app/docs/functions/writing-functions · Updated 2026-11-11

An Edge Function is a Deno program. SupaHost loads its `index.ts`, which registers a request handler with `Deno.serve`. Everything else (helper files, npm packages, routing) builds on that.

## The entry file

Every function needs a file named exactly `index.ts` at its root, and that file must call `Deno.serve`. The handler receives a standard [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and returns a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response):

```ts
Deno.serve(async (req: Request) => {
  const url = new URL(req.url);
  const lang = url.searchParams.get("lang") ?? "en";
  return Response.json({ greeting: lang === "fr" ? "Bonjour" : "Hello" });
});
```

Deno has the web platform APIs you know from browsers: `fetch`, `URL`, `crypto.subtle`, `TextEncoder`, `ReadableStream`, `FormData` and so on. TypeScript works without a build step.

Code at the top level of the module runs once when a worker boots, not on every request. Create clients and read secrets there so warm requests skip that work. Don't rely on in-memory state lasting between requests, though: workers are started and stopped as needed.

## Imports

Three kinds of import specifier work:

| Specifier | Example |
|---|---|
| `npm:` | `import Stripe from "npm:stripe@18";` |
| `jsr:` | `import { createClient } from "jsr:@supabase/supabase-js@2";` |
| `https:` | `import { format } from "https://deno.land/std@0.224.0/datetime/format.ts";` |

Pin a version in every import (`@2`, `@18`, an exact version for `https:` URLs). An unpinned import can pick up a breaking release the next time you deploy.

Imports are resolved when you deploy. SupaHost bundles the function as a check, which downloads every dependency and fails the deploy if one can't be found, so a bad import never replaces a working version. See [Import errors with esm.sh or npm packages](/kb/esm-sh-imports) if a package won't resolve.

Write full specifiers in your import statements. A `deno.json` or import map is stored like any other file, but don't count on the runtime reading it.

## Several files and folders

A function can have up to 50 files (1 MB of source in total), in folders up to 8 levels deep. In the dashboard editor, click **+ File** and enter a path such as `_shared/cors.ts` or `lib/pricing.ts`. Import them with relative paths:

```ts
// index.ts
import { corsHeaders } from "./_shared/cors.ts";
import { quote } from "./lib/pricing.ts";

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
  const { nights, room } = await req.json();
  return Response.json(quote(room, nights), { headers: corsHeaders });
});
```

```ts
// lib/pricing.ts
const RATES: Record<string, number> = { single: 80, double: 120, suite: 260 };

export function quote(room: string, nights: number) {
  const rate = RATES[room];
  if (!rate) throw new Error(`Unknown room type: ${room}`);
  return { room, nights, total: rate * nights, currency: "EUR" };
}
```

Each function's files belong to that function only. If two functions need the same helper, include the file in both. (Supabase's CLI layout has a `supabase/functions/_shared` folder next to the functions; on SupaHost, put `_shared/` inside each function instead.)

## Routing inside one function

The function sees the path after `/functions/v1`, starting with its own name. A request to `/functions/v1/bookings/42` arrives with `pathname` `/bookings/42`. That lets one function serve a small REST API. `URLPattern` is built into Deno:

```ts
import { createClient } from "jsr:@supabase/supabase-js@2";

const byId = new URLPattern({ pathname: "/bookings/:id" });
const list = new URLPattern({ pathname: "/bookings" });

Deno.serve(async (req) => {
  // Act as the caller so Row Level Security applies.
  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_ANON_KEY")!,
    { global: { headers: { Authorization: req.headers.get("Authorization") ?? "" } } },
  );

  const one = byId.exec(req.url);
  if (one && req.method === "GET") {
    const { data, error } = await supabase
      .from("bookings").select("*").eq("id", one.pathname.groups.id).maybeSingle();
    if (error) return Response.json({ error: error.message }, { status: 400 });
    return data ? Response.json(data) : Response.json({ error: "Not found" }, { status: 404 });
  }

  if (list.test(req.url) && req.method === "GET") {
    const { data, error } = await supabase.from("bookings").select("*").order("starts_at");
    if (error) return Response.json({ error: error.message }, { status: 400 });
    return Response.json(data);
  }

  return Response.json({ error: "No such route" }, { status: 404 });
});
```

From supabase-js, call a sub-path by putting it in the name: `supabase.functions.invoke("bookings/42", { method: "GET" })`.

## Reading the request

```ts
const body = await req.json();            // JSON
const text = await req.text();            // raw body (needed for webhook signatures)
const form = await req.formData();        // multipart or urlencoded forms
const file = form.get("photo") as File;   // an uploaded file
const auth = req.headers.get("Authorization");
```

A body can be read only once. If you need the raw text and the parsed JSON, read the text and call `JSON.parse` yourself.

## Errors

Return errors on purpose with a status code and a message your app can show. If your code throws and nothing catches it, the worker fails and the caller gets a `500` with `sb-error-code: WORKER_ERROR`; the stack trace goes to the function's logs. A useful pattern:

```ts
Deno.serve(async (req) => {
  try {
    const { nights } = await req.json();
    if (!Number.isInteger(nights) || nights < 1) {
      return Response.json({ error: "nights must be a positive integer" }, { status: 400 });
    }
    return Response.json({ ok: true });
  } catch (err) {
    console.error("Unexpected failure", err);
    return Response.json({ error: "Something went wrong" }, { status: 500 });
  }
});
```

## Environment variables

Read secrets and the built-in variables with `Deno.env.get("NAME")`. The built-ins are `SUPABASE_URL`, `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY` and `SUPABASE_FUNCTION_SLUG`. See [Environment variables and secrets](/docs/functions/secrets).

## Related

- [Deploying functions](/docs/functions/deploying)
- [CORS for browser calls](/docs/functions/cors)
- [Using the database from a function](/docs/functions/database-access)
- [Edge Function limits](/docs/functions/limits)
