mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Contributes to DOCS-1052 ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Resolves MDX linting errors related to "simple" where it applies. There was a couple cases that did not apply. For example, a product with "Simple" in the name. These changes are made in context, either by removing or using a more descriptive synonym like "minimal" or "basic". ## Tophatting 1. Read each of the diffs. 2. See that the text still makes sense in context. For extra due diligence, you can run `pnpm lint:mdx` locally and see the 'simple' errors that remain and whether they are worth addressing. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit * **Documentation** * Updated many guide, tutorial, and troubleshooting pages with clearer “basic”/“minimal” wording across setup steps, local testing instructions, security cautions, and RLS guidance. * Refined headings, example descriptions, and inline comments for consistency (including deployment, MCP, metrics API, and search/function phrasing). * Improved readability with small snippet formatting tweaks (whitespace plus import/comment ordering) and added a self-hosting debugging note for Envoy admin endpoints via a short-lived `curl` container. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Chris Chinchilla <chris.ward@supabase.io> Co-authored-by: Nik Richers <nrichers@gmail.com>
425 lines
10 KiB
Plaintext
425 lines
10 KiB
Plaintext
---
|
|
id: 'function-routing'
|
|
title: 'Handling Routing in Functions'
|
|
description: 'How to handle custom routing within Edge Functions.'
|
|
subtitle: 'Handle custom routing within Edge Functions.'
|
|
---
|
|
|
|
Usually, an Edge Function is written to perform a single action (e.g. write a record to the database). However, if your app's logic is split into multiple Edge Functions, requests to each action may seem slower.
|
|
|
|
Each Edge Function needs to be booted before serving a request (known as cold starts). If an action is performed less frequently (e.g. deleting a record), there is a high chance of that function experiencing a cold start.
|
|
|
|
One way to reduce cold starts and increase performance is to combine multiple actions into a single Edge Function. This way only one instance needs to be booted and it can handle multiple requests to different actions.
|
|
|
|
This allows you to:
|
|
|
|
- Reduce cold starts by combining multiple actions into one function
|
|
- Build complete REST APIs in a single function
|
|
- Improve performance by keeping one instance warm for multiple endpoints
|
|
|
|
---
|
|
|
|
For example, we can use a single Edge Function to create a typical CRUD API (create, read, update, delete records).
|
|
|
|
To combine multiple endpoints into a single Edge Function, you can use web application frameworks such as [Express](https://expressjs.com/), [Oak](https://oakserver.github.io/oak/), or [Hono](https://hono.dev).
|
|
|
|
---
|
|
|
|
## Basic routing example
|
|
|
|
Here's a basic hello world example using some popular web frameworks:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="hono"
|
|
queryGroup="framework"
|
|
>
|
|
<TabPanel id="deno" label="Deno">
|
|
|
|
```ts
|
|
import { withSupabase } from 'npm:@supabase/server@^1'
|
|
|
|
export default {
|
|
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
|
if (req.method === 'GET') {
|
|
return new Response('Hello World!')
|
|
}
|
|
const { name } = await req.json()
|
|
if (name) {
|
|
return new Response(`Hello ${name}!`)
|
|
}
|
|
return new Response('Hello World!')
|
|
}),
|
|
}
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="expressjs" label="Express">
|
|
|
|
```ts
|
|
import express from 'npm:express@4.18.2'
|
|
|
|
const app = express()
|
|
app.use(express.json())
|
|
// If you want a payload larger than 100kb, then you can tweak it here:
|
|
// app.use( express.json({ limit : "300kb" }));
|
|
|
|
const port = 3000
|
|
|
|
app.get('/hello-world', (req, res) => {
|
|
res.send('Hello World!')
|
|
})
|
|
|
|
app.post('/hello-world', (req, res) => {
|
|
const { name } = req.body
|
|
res.send(`Hello ${name}!`)
|
|
})
|
|
|
|
app.listen(port, () => {
|
|
console.log(`Example app listening on port ${port}`)
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="oak" label="Oak">
|
|
|
|
```ts
|
|
import { Application } from 'jsr:@oak/oak@15/application'
|
|
import { Router } from 'jsr:@oak/oak@15/router'
|
|
|
|
const router = new Router()
|
|
|
|
router.get('/hello-world', (ctx) => {
|
|
ctx.response.body = 'Hello world!'
|
|
})
|
|
|
|
router.post('/hello-world', async (ctx) => {
|
|
const { name } = await ctx.request.body.json()
|
|
ctx.response.body = `Hello ${name}!`
|
|
})
|
|
|
|
const app = new Application()
|
|
app.use(router.routes())
|
|
app.use(router.allowedMethods())
|
|
|
|
app.listen({ port: 3000 })
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="hono" label="Hono">
|
|
|
|
```ts
|
|
import { Hono } from 'jsr:@hono/hono'
|
|
|
|
const app = new Hono()
|
|
|
|
app.post('/hello-world', async (c) => {
|
|
const { name } = await c.req.json()
|
|
return new Response(`Hello ${name}!`)
|
|
})
|
|
|
|
app.get('/hello-world', (c) => {
|
|
return new Response('Hello World!')
|
|
})
|
|
|
|
export default { fetch: app.fetch }
|
|
```
|
|
|
|
To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth).
|
|
|
|
</TabPanel>
|
|
|
|
</Tabs>
|
|
|
|
<Admonition type="caution">
|
|
|
|
Within Edge Functions, paths should always be prefixed with the function name (in this case `hello-world`).
|
|
|
|
</Admonition>
|
|
|
|
---
|
|
|
|
## Using route parameters
|
|
|
|
You can use route parameters to capture values at specific URL segments (e.g. `/tasks/:taskId/notes/:noteId`).
|
|
|
|
Keep in mind paths must be prefixed by function name. Route parameters can only be used after the function name prefix.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="deno"
|
|
queryGroup="framework"
|
|
>
|
|
|
|
<TabPanel id="deno" label="Deno">
|
|
|
|
```ts
|
|
import { withSupabase } from 'npm:@supabase/server@^1'
|
|
|
|
interface Task {
|
|
id: string
|
|
name: string
|
|
}
|
|
|
|
let tasks: Task[] = []
|
|
|
|
const router = new Map<string, (req: Request) => Promise<Response>>()
|
|
|
|
async function getAllTasks(): Promise<Response> {
|
|
return new Response(JSON.stringify(tasks))
|
|
}
|
|
|
|
async function getTask(id: string): Promise<Response> {
|
|
const task = tasks.find((t) => t.id === id)
|
|
if (task) {
|
|
return new Response(JSON.stringify(task))
|
|
} else {
|
|
return new Response('Task not found', { status: 404 })
|
|
}
|
|
}
|
|
|
|
async function createTask(req: Request): Promise<Response> {
|
|
const id = Math.random().toString(36).substring(7)
|
|
const task = { id, name: '' }
|
|
tasks.push(task)
|
|
return new Response(JSON.stringify(task), { status: 201 })
|
|
}
|
|
|
|
async function updateTask(id: string, req: Request): Promise<Response> {
|
|
const index = tasks.findIndex((t) => t.id === id)
|
|
if (index !== -1) {
|
|
tasks[index] = { ...tasks[index] }
|
|
return new Response(JSON.stringify(tasks[index]))
|
|
} else {
|
|
return new Response('Task not found', { status: 404 })
|
|
}
|
|
}
|
|
|
|
async function deleteTask(id: string): Promise<Response> {
|
|
const index = tasks.findIndex((t) => t.id === id)
|
|
if (index !== -1) {
|
|
tasks.splice(index, 1)
|
|
return new Response('Task deleted successfully')
|
|
} else {
|
|
return new Response('Task not found', { status: 404 })
|
|
}
|
|
}
|
|
|
|
export default {
|
|
fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
|
|
const url = new URL(req.url)
|
|
const method = req.method
|
|
// Extract the last part of the path as the command
|
|
const command = url.pathname.split('/').pop()
|
|
// Assuming the last part of the path is the task ID
|
|
const id = command
|
|
try {
|
|
switch (method) {
|
|
case 'GET':
|
|
if (id) {
|
|
return getTask(id)
|
|
} else {
|
|
return getAllTasks()
|
|
}
|
|
case 'POST':
|
|
return createTask(req)
|
|
case 'PUT':
|
|
if (id) {
|
|
return updateTask(id, req)
|
|
} else {
|
|
return new Response('Bad Request', { status: 400 })
|
|
}
|
|
case 'DELETE':
|
|
if (id) {
|
|
return deleteTask(id)
|
|
} else {
|
|
return new Response('Bad Request', { status: 400 })
|
|
}
|
|
default:
|
|
return new Response('Method Not Allowed', { status: 405 })
|
|
}
|
|
} catch (error) {
|
|
return new Response(`Internal Server Error: ${error}`, { status: 500 })
|
|
}
|
|
}),
|
|
}
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="expressjs" label="Express">
|
|
|
|
```ts
|
|
import express from 'npm:express@4.18.2'
|
|
|
|
const app = express()
|
|
app.use(express.json())
|
|
|
|
app.get('/tasks', async (req, res) => {
|
|
// return all tasks
|
|
})
|
|
|
|
app.post('/tasks', async (req, res) => {
|
|
// create a task
|
|
})
|
|
|
|
app.get('/tasks/:id', async (req, res) => {
|
|
const id = req.params.id
|
|
const task = {} // get task
|
|
|
|
res.json(task)
|
|
})
|
|
|
|
app.patch('/tasks/:id', async (req, res) => {
|
|
const id = req.params.id
|
|
// modify task
|
|
})
|
|
|
|
app.delete('/tasks/:id', async (req, res) => {
|
|
const id = req.params.id
|
|
// delete task
|
|
})
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="oak" label="Oak">
|
|
|
|
```ts
|
|
import { Application } from 'jsr:@oak/oak/application'
|
|
import { Router } from 'jsr:@oak/oak/router'
|
|
|
|
const router = new Router()
|
|
|
|
let tasks: { [id: string]: any } = {}
|
|
|
|
router
|
|
.get('/tasks', (ctx) => {
|
|
ctx.response.body = Object.values(tasks)
|
|
})
|
|
.post('/tasks', async (ctx) => {
|
|
const body = ctx.request.body()
|
|
const { name } = await body.value
|
|
const id = Math.random().toString(36).substring(7)
|
|
tasks[id] = { id, name }
|
|
ctx.response.body = tasks[id]
|
|
})
|
|
.get('/tasks/:id', (ctx) => {
|
|
const id = ctx.params.id
|
|
const task = tasks[id]
|
|
if (task) {
|
|
ctx.response.body = task
|
|
} else {
|
|
ctx.response.status = 404
|
|
ctx.response.body = 'Task not found'
|
|
}
|
|
})
|
|
.patch('/tasks/:id', async (ctx) => {
|
|
const id = ctx.params.id
|
|
const body = ctx.request.body()
|
|
const updates = await body.value
|
|
const task = tasks[id]
|
|
if (task) {
|
|
tasks[id] = { ...task, ...updates }
|
|
ctx.response.body = tasks[id]
|
|
} else {
|
|
ctx.response.status = 404
|
|
ctx.response.body = 'Task not found'
|
|
}
|
|
})
|
|
.delete('/tasks/:id', (ctx) => {
|
|
const id = ctx.params.id
|
|
if (tasks[id]) {
|
|
delete tasks[id]
|
|
ctx.response.body = 'Task deleted successfully'
|
|
} else {
|
|
ctx.response.status = 404
|
|
ctx.response.body = 'Task not found'
|
|
}
|
|
})
|
|
|
|
const app = new Application()
|
|
app.use(router.routes())
|
|
app.use(router.allowedMethods())
|
|
|
|
app.listen({ port: 3000 })
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="hono" label="Hono">
|
|
|
|
```ts
|
|
import { Hono } from 'jsr:@hono/hono'
|
|
|
|
// You can set the basePath with Hono
|
|
const functionName = 'tasks'
|
|
const app = new Hono().basePath(`/${functionName}`)
|
|
|
|
// /tasks/id
|
|
app.get('/:id', async (c) => {
|
|
const id = c.req.param('id')
|
|
const task = {} // Fetch task by id here
|
|
if (task) {
|
|
return new Response(JSON.stringify(task))
|
|
} else {
|
|
return new Response('Task not found', { status: 404 })
|
|
}
|
|
})
|
|
|
|
app.patch('/:id', async (c) => {
|
|
const id = c.req.param('id')
|
|
const body = await c.req.body()
|
|
const updates = body.value
|
|
const task = {} // Fetch task by id here
|
|
if (task) {
|
|
Object.assign(task, updates)
|
|
return new Response(JSON.stringify(task))
|
|
} else {
|
|
return new Response('Task not found', { status: 404 })
|
|
}
|
|
})
|
|
|
|
app.delete('/:id', async (c) => {
|
|
const id = c.req.param('id')
|
|
const task = {} // Fetch task by id here
|
|
if (task) {
|
|
// Delete task
|
|
return new Response('Task deleted successfully')
|
|
} else {
|
|
return new Response('Task not found', { status: 404 })
|
|
}
|
|
})
|
|
|
|
export default { fetch: app.fetch }
|
|
```
|
|
|
|
To add Supabase auth per route, use the Hono adapter from `npm:@supabase/server@^1/adapters/hono`. See [Securing Edge Functions](/docs/guides/functions/auth).
|
|
|
|
</TabPanel>
|
|
|
|
</Tabs>
|
|
|
|
---
|
|
|
|
{/* supa-mdx-lint-disable Rule001HeadingCase */}
|
|
|
|
## URL Patterns API
|
|
|
|
If you prefer not to use a web framework, you can directly use [URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) within your Edge Functions to implement routing.
|
|
|
|
This works well for small apps with only a couple of routes:
|
|
|
|
<$CodeSample
|
|
path="/edge-functions/supabase/functions/restful-tasks/index.ts"
|
|
lines={[[48, -1]]}
|
|
meta="restful-tasks/index.ts"
|
|
/>
|