On this page

Write functions

A function is a module that calls Deno.serve with a handler from Request to Response.

#The handler

Your entrypoint (index.ts by default) calls Deno.serve once. The handler receives a standard Request and returns a Response (or a promise of one).

index.ts
Deno.serve(async (req) => {
  const { name = 'world' } = await req.json().catch(() => ({}))
  return Response.json({ message: `Hello ${name}!` })
})

The request URL path starts with the function name: a call to /functions/v1/my-api/items/42?x=1 arrives as /my-api/items/42?x=1. Use it to serve several routes from one function:

index.ts
import { Hono } from 'jsr:@hono/hono@4'

// The request path starts with the function name: /my-api/...
const app = new Hono().basePath('/my-api')

app.get('/items/:id', (c) => c.json({ id: c.req.param('id') }))
app.post('/items', async (c) => c.json(await c.req.json(), 201))

Deno.serve(app.fetch)

#Imports

TypeScript
// npm and JSR packages, and modules from URLs
import Stripe from 'npm:stripe@17'
import { Hono } from 'jsr:@hono/hono@4'
import { createClient } from 'https://stitchdb.com/sdk/stitchdb-js.mjs'

// Your own files, deployed with the function
import { slugify } from './lib/text.ts'

Packages are downloaded when the function first starts and cached on the server. Pin versions (npm:stripe@17) so a new release can't change your function unexpectedly.

#Several files and an import map

A function can have up to 200 files. Add files in the Code tab (+ File) or upload them with the API. A deno.json (or import_map.json) next to the entrypoint is used as the import map:

deno.json
{
  "imports": {
    "zod": "npm:zod@3",
    "@/": "./lib/"
  }
}

#Streaming and background work

Responses stream to the caller as you produce them, so server-sent events work:

TypeScript
Deno.serve(() => {
  const body = new ReadableStream({
    async start(controller) {
      for (let i = 1; i <= 5; i++) {
        controller.enqueue(new TextEncoder().encode(`data: tick ${i}\n\n`))
        await new Promise((r) => setTimeout(r, 1000))
      }
      controller.close()
    },
  })
  return new Response(body, { headers: { 'Content-Type': 'text/event-stream' } })
})

To answer first and keep working, hand a promise to EdgeRuntime.waitUntil(). It runs until it settles or the wall-clock limit is reached:

TypeScript
Deno.serve(async (req) => {
  const payload = await req.json()
  // Answer right away; the work continues after the response (within the wall-clock limit).
  EdgeRuntime.waitUntil(fetch('https://hooks.example.com/ingest', { method: 'POST', body: JSON.stringify(payload) }))
  return new Response('accepted', { status: 202 })
})

#What is not available

  • Starting processes (Deno.Command) and native libraries (FFI).
  • The server's file system. Each request may write temporary files under /tmp; they are not kept.
  • Private and internal network addresses, UDP and SMTP on port 25. Send email through an HTTPS API (see Examples).

#Errors

StatusCodeMeaning
401UNAUTHORIZED_NO_AUTH_HEADER / UNAUTHORIZED_INVALID_JWTJWT verification is on and the request has no valid token.
404NOT_FOUNDNo function with that name.
429QUOTA_EXCEEDEDThe monthly invocations of your plan are used up (or the API rate limit was hit).
500WORKER_ERRORThe function threw before answering. See its logs.
503BOOT_ERRORThe function failed to start: a syntax or import error. See its logs.
504IDLE_TIMEOUTThe function did not start answering in time.
546WORKER_LIMITThe function was stopped because it used more CPU time, memory or wall-clock time than your plan allows.