mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
424 lines
11 KiB
Plaintext
424 lines
11 KiB
Plaintext
---
|
|
id: 'function-routing'
|
|
title: 'Handling Routing in Functions'
|
|
description: 'How to handle custom routing within Edge Functions.'
|
|
subtitle: 'How to handle custom routing within Edge Functions.'
|
|
---
|
|
|
|
Usually, an Edge Function is written to perform a single action (eg: 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.
|
|
This is because each Edge Function needs to be booted before serving a request (known as cold starts). If an action is performed less frequently (eg: deleting a record), there is a high-chance of that function experiencing a cold-start.
|
|
|
|
One way to reduce the cold starts and increase performance of your app is to combine multiple actions into a single Edge Function. This way only one instance of the Edge Function needs to be booted and it can handle multiple requests to different actions.
|
|
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).
|
|
|
|
Let's dive into some examples.
|
|
|
|
## Routing with frameworks
|
|
|
|
Here's a simple hello world example using some popular web frameworks.
|
|
|
|
Create a new function called `hello-world` using Supabase CLI:
|
|
|
|
```bash
|
|
supabase functions new hello-world
|
|
```
|
|
|
|
Copy and paste the following code:
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="hono"
|
|
queryGroup="framework"
|
|
>
|
|
<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!')
|
|
});
|
|
|
|
Deno.serve(app.fetch);
|
|
````
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="deno" label="Deno">
|
|
```ts
|
|
Deno.serve(async (req) => {
|
|
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>
|
|
|
|
</Tabs>
|
|
|
|
You will notice in the above example, we created two routes - `GET` and `POST`. The path for both routes are defined as `/hello-world`.
|
|
If you run a server outside of Edge Functions, you'd usually set the root path as `/` .
|
|
However, within Edge Functions, paths should always be prefixed with the function name (in this case `hello-world`).
|
|
|
|
You can deploy the function to Supabase via:
|
|
|
|
```bash
|
|
supabase functions deploy hello-world
|
|
```
|
|
|
|
Once the function is deployed, you can try to call the two endpoints using cURL (or Postman).
|
|
|
|
```bash
|
|
# https://supabase.com/docs/guides/functions/deploy#invoking-remote-functions
|
|
curl --request GET 'https://<project_ref>.supabase.co/functions/v1/hello-world' \
|
|
--header 'Authorization: Bearer ANON_KEY' \
|
|
```
|
|
|
|
This should print the response as `Hello World!`, meaning it was handled by the `GET` route.
|
|
|
|
Similarly, we can make a request to the `POST` route.
|
|
|
|
```bash cURL
|
|
# https://supabase.com/docs/guides/functions/deploy#invoking-remote-functions
|
|
curl --request POST 'https://<project_ref>.supabase.co/functions/v1/hello-world' \
|
|
--header 'Authorization: Bearer ANON_KEY' \
|
|
--header 'Content-Type: application/json' \
|
|
--data '{ "name":"Foo" }'
|
|
```
|
|
|
|
We should see a response printing `Hello Foo!`.
|
|
|
|
## Using route parameters
|
|
|
|
We can use route parameters to capture values at specific URL segments (eg: `/tasks/:taskId/notes/:noteId`).
|
|
|
|
Here's an example Edge Function implemented using the Framework for managing tasks using route parameters.
|
|
Keep in mind paths must be prefixed by function name (ie. `tasks` in this example). Route parameters can only be used after the function name prefix.
|
|
|
|
<Tabs
|
|
scrollable
|
|
size="small"
|
|
type="underlined"
|
|
defaultActiveId="Hono"
|
|
queryGroup="framework"
|
|
>
|
|
<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 })
|
|
}
|
|
})
|
|
|
|
Deno.serve(app.fetch)
|
|
```
|
|
|
|
</TabPanel>
|
|
|
|
<TabPanel id="deno" label="Deno">
|
|
|
|
```ts
|
|
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 })
|
|
}
|
|
}
|
|
|
|
Deno.serve(async (req) => {
|
|
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>
|
|
</Tabs>
|
|
|
|
## URL patterns API
|
|
|
|
If you prefer not to use a web framework, you can directly use [URLPattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) within your Edge Functions to implement routing.
|
|
This is ideal for small apps with only couple of routes and you want to have a custom matching algorithm.
|
|
|
|
Here is an example Edge Function using URL Patterns API: https://github.com/supabase/supabase/blob/master/examples/edge-functions/supabase/functions/restful-tasks/index.ts
|