PUT-1960 worker recipes

This commit is contained in:
Neal Shah committed 2026-09-29 17:11:06 -04:00
1 parent cd37cf147f
commit e4d7053682
5 files changed
+461

No files matched your search

+12
View File
@@ -986,6 +986,18 @@ const examples = [
slug: 'workers-exec',
source: '/playground/examples/workers-exec.html',
},
{
title: 'Build an API',
description: 'Deploy a worker whose routes store notes in the calling user\'s own key-value store with user.puter, and call it with puter.workers.exec(). Run and modify this example directly in your browser.',
slug: 'workers-build-an-api',
source: '/playground/examples/workers-build-an-api.html',
},
{
title: 'Update a worker in place',
description: 'Ship new code to a deployed worker at the same URL by overwriting its source file with Puter.js workers API. Run and modify this example directly in your browser.',
slug: 'workers-deploy-from-code-update',
source: '/playground/examples/workers-deploy-from-code-update.html',
},
],
},
];
@@ -0,0 +1,62 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>Notes</h3>
<input id="text" placeholder="Write a note">
<button id="save">Save note</button>
<button id="list">Show my notes</button>
<script>
// Every note is stored in the calling user's own KV through user.puter,
// so each user only ever sees their own notes.
const workerCode = `
router.post('/notes', async ({ request, user }) => {
if (!user) return new Response('sign in required', { status: 401 });
const { text } = await request.json();
if (typeof text !== 'string' || text.length === 0) {
return new Response('text is required', { status: 400 });
}
const note = { id: crypto.randomUUID(), text, at: Date.now() };
await user.puter.kv.set('notes:' + note.id, note);
return note;
});
router.get('/notes', async ({ user }) => {
if (!user) return new Response('sign in required', { status: 401 });
const rows = await user.puter.kv.list('notes:', true);
return rows.map(row => row.value);
});
`;
let api;
async function deploy () {
await puter.fs.write('notes-api.js', workerCode);
const deployment = await puter.workers.create(puter.randName(), 'notes-api.js');
puter.print(`Deployed to ${deployment.url}, waiting for it to go live...<br>`);
// A new worker takes a few seconds to reach every edge server.
await new Promise(resolve => setTimeout(resolve, 10000));
return deployment.url;
}
document.getElementById('save').addEventListener('click', async () => {
api ??= deploy();
// exec() sends the signed-in user, which is what gives the worker user.puter.
const res = await puter.workers.exec(`${await api}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: document.getElementById('text').value }),
});
puter.print(`${res.status}: ${await res.text()}<br>`);
});
document.getElementById('list').addEventListener('click', async () => {
api ??= deploy();
const notes = await (await puter.workers.exec(`${await api}/notes`)).json();
puter.print(`You have ${notes.length} note(s): ${notes.map(n => n.text).join(', ')}<br>`);
});
</script>
</body>
</html>
@@ -0,0 +1,41 @@
<html>
<body>
<script src="https://js.puter.com/v2/"></script>
<h3>App builder: project API</h3>
<input id="greeting" value="Welcome back">
<button id="ship">Ship change</button>
<button id="call">Call the worker</button>
<script>
const name = `project-${puter.randName()}`;
// The route greets whoever calls it, using their own Puter account.
const code = greeting => `
router.get('/', async ({ user }) => {
if (!user) return new Response('sign in required', { status: 401 });
const { username } = await user.puter.getUser();
return ${JSON.stringify(greeting)} + ', ' + username;
});
`;
const deployed = (async () => {
await puter.fs.write(`${name}.js`, code('Hello'));
const { url } = await puter.workers.create(name, `${name}.js`);
puter.print(`Created ${url}, live within about 30 seconds<br>`);
return url;
})();
document.getElementById('ship').addEventListener('click', async () => {
await deployed;
const info = await puter.workers.get(name);
// Overwriting the source file redeploys at the same URL.
await puter.fs.write(info.file_path, code(document.getElementById('greeting').value));
puter.print(`Shipped. ${info.url} serves the new code within about 30 seconds.<br>`);
});
document.getElementById('call').addEventListener('click', async () => {
const res = await puter.workers.exec(await deployed);
puter.print(`${res.status}: ${await res.text()}<br>`);
});
</script>
</body>
</html>
@@ -0,0 +1,217 @@
---
title: Build an API with a Worker
description: "Learn how to write the routes of a serverless worker with Puter.js, reading requests, storing data in the calling user's own account and sending back JSON or errors."
tags: [workers, kv, auth]
order: 38
---
A [serverless worker](/Workers/) is JavaScript that runs in the cloud instead of
in the user's browser. It answers HTTP requests at its own URL, such as
`https://notes-api.puter.work`, and you write it as a list of routes, each one a
path and the function that handles it.
When your app calls a worker with
[`puter.workers.exec()`](/Workers/exec/), the worker knows which user is calling
and gets that user's Puter account as `user.puter`. A route can then read and
write the user's own [key-value store](/KV/), files and AI, the same way your
app does in the browser. The data stays in the user's account, and the usage is
billed to them under the [User-Pays model](/user-pays-model/), so you get a
backend without paying for its storage.
## Define a Route
The `router` object is available in every worker. Register a route with the
method's name and a path, and return what the caller should get back:
```js
router.get('/me', async ({ user }) => {
if (!user) {
return new Response('sign in required', { status: 401 });
}
const { username } = await user.puter.getUser();
return { username };
});
```
There is one method per HTTP verb: `router.get()`, `router.post()`,
`router.put()`, `router.delete()` and `router.options()`.
`user` is only there when the request came through
[`puter.workers.exec()`](/Workers/exec/), so every route that uses it starts by
checking for it.
## Store Data in the User's Account
To save something for the caller, write it with `user.puter.kv`. Read the JSON
body with `request.json()`:
```js
router.post('/notes', async ({ request, user }) => {
if (!user) {
return new Response('sign in required', { status: 401 });
}
const { text } = await request.json();
if (typeof text !== 'string' || text.length === 0) {
return new Response('text is required', { status: 400 });
}
const note = { id: crypto.randomUUID(), text, at: Date.now() };
await user.puter.kv.set(`notes:${note.id}`, note);
return note;
});
```
Each user's notes land in their own store, so one user can never read or
overwrite another's, whatever the request body says. These are the same keys
your app sees through [`puter.kv`](/KV/) in the browser, so the app can also
read a note directly.
## Read the Request
Each handler receives one object. Its `request` is a standard
[`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request), and its
`params` holds the parts of the path you marked with a colon:
```js
router.get('/notes/:id', async ({ params, user }) => {
if (!user) {
return new Response('sign in required', { status: 401 });
}
const note = await user.puter.kv.get(`notes:${params.id}`);
if (!note) {
return new Response('note not found', { status: 404 });
}
return note;
});
```
Captured values are always strings, so convert them yourself when you need a
number.
Query strings are not part of the path. To read one, parse the request URL:
```js
router.get('/notes', async ({ request, user }) => {
if (!user) {
return new Response('sign in required', { status: 401 });
}
const q = new URL(request.url).searchParams.get('q') ?? '';
const rows = await user.puter.kv.list('notes:', true);
return rows
.map(row => row.value)
.filter(note => note.text.includes(q));
});
```
## Send a Response
A handler that returns a plain object or array is sent as JSON, and one that
returns a string is sent as text. To choose the status code or the headers,
return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response),
as the `401`, `400` and `404` answers above do.
The worker adds CORS headers to every response, so your app can call it from
any origin without extra setup.
## Handle Unknown Paths
A wildcard route matches the rest of the path. Register one last, so it only
runs when no other route matched, and use it to answer with a `404`:
```js
router.get('/*path', async ({ params }) => {
return new Response(`no route for /${params.path}`, { status: 404 });
});
```
The wildcard needs a name, such as `*path`. A bare `*` is read as a literal
character and does not match anything else.
## Routes Without a User
A worker is also an ordinary HTTP endpoint, so it can serve callers that have
never heard of Puter: a webhook from another service, a `curl` in a script, or a
page that doesn't load Puter.js. These routes never touch `user`.
A route can do its work and answer, with nothing to store:
```js
router.get('/slugify', async ({ request }) => {
const text = new URL(request.url).searchParams.get('text') ?? '';
return text
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
});
```
Any HTTP client can call it:
```sh
curl "https://notes-api.puter.work/slugify?text=Hello%20World"
# hello-world
```
To keep what an outside service sends you, store it in your own account with
`me.puter`, which is available to every request whoever made it:
```js
router.post('/webhooks/payments', async ({ request }) => {
const event = await request.json();
await me.puter.kv.set(`payments:${event.id}`, event);
return { received: true };
});
```
A worker can also call other APIs with `fetch()`, which lets it reshape a
third-party response before your app sees it:
```js
router.get('/weather/:city', async ({ params }) => {
const res = await fetch(`https://wttr.in/${encodeURIComponent(params.city)}?format=j1`);
const data = await res.json();
return { city: params.city, tempC: data.current_condition[0].temp_C };
});
```
Anyone who knows the URL can call a route like these, so don't put anything
behind one that should be private.
## Call It From Your App
Once the worker is [deployed](/Workers/#deployment), call it with
[`puter.workers.exec()`](/Workers/exec/). It takes the same arguments as
`fetch()` and adds the signed-in user's session, which is what gives the worker
`user.puter`:
```js
const API = 'https://notes-api.puter.work';
const res = await puter.workers.exec(`${API}/notes`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: 'Buy milk' }),
});
const note = await res.json(); // { id: '5f0c…', text: 'Buy milk', at: 1788827048741 }
const notes = await (await puter.workers.exec(`${API}/notes?q=milk`)).json();
```
A plain `fetch()` of the same URL carries no session, so `user` is undefined
and the notes routes answer `401`. It works for
[routes without a user](#routes-without-a-user), which is how
callers without Puter.js reach them.
To keep data that every user reads and writes together, such as a leaderboard,
store it with `me.puter` instead. See
[Store server-side data](/recipes/store-server-side-data/).
@@ -0,0 +1,129 @@
---
title: Deploy a Worker from Your App
description: "Learn how to create, update, list and delete serverless workers from your own code with Puter.js, instead of publishing them by hand."
tags: [workers]
order: 42
---
You can publish a [serverless worker](/Workers/) by hand from puter.com. You
can also do it from code, with the [Workers API](/Workers/#workers-api). Your
app then creates a worker, updates its code and removes it, the same way it
writes files.
This lets you build tools that ship backends for their users, such as an app
builder that gives each project its own API.
## Create a Worker
A worker is deployed from a JavaScript file in your Puter account. Write the
code with [`puter.fs.write()`](/FS/write/), then pass the file to
[`puter.workers.create()`](/Workers/create/) along with a name:
```js
await puter.fs.write('notes-api.js', `
router.get('/me', async ({ user }) => {
const { username } = await user.puter.getUser();
return { username };
});
`);
const deployment = await puter.workers.create('notes-api', 'notes-api.js');
deployment.url; // 'https://notes-api.puter.work'
```
Give it 5 to 30 seconds before the first request, while the worker spreads to
every edge server. Then call it with
[`puter.workers.exec()`](/Workers/exec/), which sends the signed-in user along
so the route can use `user.puter`:
```js
const res = await puter.workers.exec(`${deployment.url}/me`);
await res.json(); // { username: 'grace' }
```
[Build an API with a worker](/recipes/workers-build-an-api/) covers what to put
in the routes.
Worker names are global, like subdomains, and are stored in lowercase. A name
can use letters, numbers, hyphens and underscores. When another account already
has the name, `create()` rejects, so pick another one or use
[`puter.randName()`](/Utils/randName/). The account deploying the worker needs a
verified email address.
## Update Its Code
A worker keeps its name and URL for as long as it exists. To change what it
runs, overwrite its source file. [`puter.workers.get()`](/Workers/get/) tells
you where that file is:
```js
const info = await puter.workers.get('notes-api');
await puter.fs.write(info.file_path, updatedCode);
```
Writing the file redeploys the worker at the same URL, so anything already
calling it keeps working.
Don't create a new worker with a new name to ship a change. The old one stays
online at its old URL, and every caller has to be pointed at the new one.
## Find Your Workers
To show the workers in your account, call
[`puter.workers.list()`](/Workers/list/):
```js
const workers = await puter.workers.list();
for (const worker of workers) {
console.log(worker.name, worker.url);
}
```
To look up one worker by name, use [`puter.workers.get()`](/Workers/get/). It
resolves to `undefined` when there is no worker with that name, which makes it
a quick existence check before a deploy:
```js
const existing = await puter.workers.get('notes-api');
if (!existing) {
await puter.workers.create('notes-api', 'notes-api.js');
}
```
## Keep Workers Apart
Inside a worker, [`me.puter`](/Workers/router/#integration-with-puter-js)
reaches the key-value store and files of the app the worker runs as. When your
app creates workers, they run as your app by default, so they all share that
data with each other and with your app.
To give each worker its own data, pass `sandbox: true`:
```js
await puter.workers.create('project-alpha-api', 'alpha.js', { sandbox: true });
await puter.workers.create('project-beta-api', 'beta.js', { sandbox: true });
```
Each worker now runs as its own app, so a key one of them writes is not visible
to the other. Decide this before the worker stores anything, because changing
it later does not move data that was already written.
## Delete a Worker
To take a worker offline, call [`puter.workers.delete()`](/Workers/delete/)
with its name:
```js
await puter.workers.delete('notes-api');
```
Its URL stops answering, and the name is free to use again. The source file
stays in your account.
The link also works the other way. Deleting a worker's source file with
[`puter.fs.delete()`](/FS/delete/) deletes the worker too, so keep the file for
as long as the worker should stay online.