router
objectPuter workers use a router-based system to handle HTTP requests. The router
object is automatically available in your worker code and provides methods to define API endpoints.
router.post('/my-endpoint', async ({ request, user, params }) => {
return { message: 'Hello, World!' };
});
Route handlers receive structured parameters:
request
- The incoming HTTP request.user
- The user object, contains user.puter
(available when called via puter.workers.exec()
)user.puter
- The user's Puter resources (KV, FS, AI, etc.)params
- URL parameters (for dynamic routes)me
- The deployer's Puter object (your own Puter resources for KV, FS, AI, etc.)When writing worker code, you have access to several global objects:
router
- The router object for defining API endpointsme.puter
- The deployer's Puter object (your own Puter resources for KV, FS, AI, etc.)Note: me.puter
refers to the deployer's (your) Puter resources, while user.puter
refers to the user's resources when they execute your worker with their own token.
The router object supports standard HTTP methods and provides a clean way to organize your API endpoints.
router.get(path, handler)
- Handle GET requestsrouter.post(path, handler)
- Handle POST requests router.put(path, handler)
- Handle PUT requestsrouter.delete(path, handler)
- Handle DELETE requestsrouter.options(path, handler)
- Handle OPTIONS requests// Simple GET endpoint
router.get('/api/hello', async ({ request }) => {
return { message: 'Hello, World!' };
});
The example above is a simple GET endpoint that returns a JSON object with a message.
You can access the request data in the handler function. A request can be a JSON body, form data, URL parameters, or headers.
JSON Body
router.post('/api/user', async ({ request }) => {
// Get JSON body
const body = await request.json();
return { processed: true };
});
Form Data
router.post('/api/user', async ({ request }) => {
// Get form data
const formData = await request.formData();
return { processed: true };
});
URL Parameters
router.post('/api/user*tag', async ({ request }) => {
// Get URL parameters
const url = new URL(request.url);
const queryParam = url.searchParams.get('param');
return { processed: true };
});
Headers
router.post('/api/user', async ({ request }) => {
// Get headers
const contentType = request.headers.get('content-type');
return { processed: true };
});
Use :paramName
in your route path to capture dynamic segments:
// Dynamic route with parameters
router.get('/api/posts/:category/:id', async ({request, params }) => {
const { category, id } = params;
return { category, id };
});
JSON
router.get('/api/simple', async ({ request }) => {
return { status: 'ok' }; // Automatically converted to JSON
});
Plain Text
router.get('/api/text', async ({ request }) => {
return 'Hello World'; // Returns plain text
});
Blob
router.get('/api/blob', async ({ request }) => {
return new Blob(['Hello World'], { type: 'text/plain' });
});
Uint8Array
router.get('/api/uint8array', async ({ request }) => {
return new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]);
});
Binary stream
router.get('/api/binary-stream', async ({ request }) => {
return new ReadableStream({
start(controller) {
controller.enqueue(new Uint8Array([72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100]));
controller.close();
}
});
});
Custom Response Objects
router.get('/api/custom', async ({ request }) => {
return new Response(JSON.stringify({ data: 'custom' }), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Custom-Header': 'value'
}
});
});
You can also return custom error responses. To do so, you can use the Response
object and set the status code and headers.
router.post('/api/risky-operation', async ({ request }) => {
try {
const body = await request.json();
const result = await someRiskyOperation(body);
return { success: true, result };
} catch (error) {
return new Response(JSON.stringify({
error: 'Operation failed',
message: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
});
File System Operations
router.post('/api/upload', async ({ request }) => {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return new Response(JSON.stringify({ error: 'No file provided' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const fileName = `upload-${Date.now()}-${file.name}`;
await me.puter.fs.write(fileName, file);
return {
uploaded: true,
fileName,
originalName: file.name,
size: file.size
};
});
Key-Value Store (NoSQL Database)
router.post('/api/kv/set', async ({ request }) => {
const { key, value } = await request.json();
if (!key || value === undefined) {
return new Response(JSON.stringify({ error: 'Key and value required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
await me.puter.kv.set("myscope_" + key, value); // add a mandatory prefix so this wont blindly read the KV of the user's other data
return { saved: true, key };
});
router.get('/api/kv/get/:key', async ({ request, params }) => {
const key = params.key;
const value = await me.puter.kv.get("myscope_" + key); // use the same prefix
if (!value) {
return new Response(JSON.stringify({ error: 'Key not found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
return { key, value: value };
});
AI
router.post('/api/chat', async ({ request, user }) => {
const { message } = await request.json();
if (!message) {
return new Response(JSON.stringify({ error: 'Message required' }), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
// Require user authentication to prevent abuse
if (!user || !user.puter) {
return new Response(JSON.stringify({
error: 'Authentication required',
message: 'This endpoint requires user authentication. Call this worker via puter.workers.exec() with your user token to use your own AI resources.'
}), {
status: 401,
headers: { 'Content-Type': 'application/json' }
});
}
try {
// Use user's AI resources
const aiResponse = await user.puter.ai.chat(message);
// Store chat history in developer's KV for analytics
const chatHistory = {
userId: user.id || 'unknown',
message,
response: aiResponse,
timestamp: new Date().toISOString(),
usedUserAI: true
};
await me.puter.kv.set(`chat_${Date.now()}`, (chatHistory));
return {
originalMessage: message,
aiResponse,
usedUserAI: true
};
} catch (error) {
return new Response(JSON.stringify({
error: 'AI service error',
message: error.message
}), {
status: 500,
headers: { 'Content-Type': 'application/json' }
});
}
});
The user
object is available when the worker is executed via puter.workers.exec()
in the frontend and contains the user's own Puter resources. This allows you to use the user's resources (KV, FS, AI, etc.) instead of your own.
Always include a catch-all route for unmatched paths:
router.get('/*page', async ({ request, params }) => {
const requestedPath = params.page;
return new Response(JSON.stringify({
error: 'Not found',
path: requestedPath,
message: 'The requested endpoint does not exist',
availableEndpoints: [
'/api/hello',
'/api/data',
'/api/upload'
]
}), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
});
Here's a complete worker with multiple endpoints demonstrating various router patterns:
// Health check
router.get('/health', async () => {
return {
status: 'ok',
timestamp: new Date().toISOString()
};
});
// User management API
router.post('/api/users', async ({ request, user }) => {
const userInfo = await user.puter.getUser();
// Store user data
const userId = `user_${Date.now()}`;
await me.puter.kv.set(userId, {email: userInfo.email, name: userInfo.username});
return { userId, user: {email: userInfo.email, username: userInfo.username, uuid: userInfo.uuid} };
});
router.get('/api/users/:id', async ({ params }) => {
const userId = params.id;
if (!userId.startsWith("user_")) // security check
return new Response("Invalid userID!")
const userData = await me.puter.kv.get(userId);
if (!userData) {
return new Response(JSON.stringify({
error: 'User not found'
}), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
}
return { userId, user: userData };
});
// File operations
router.post('/api/files/upload', async ({ request }) => {
const formData = await request.formData();
const file = formData.get('file');
if (!file) {
return new Response(JSON.stringify({
error: 'No file provided'
}), {
status: 400,
headers: { 'Content-Type': 'application/json' }
});
}
const fileName = `upload-${Date.now()}-${file.name}`;
await me.puter.fs.write(fileName, file);
return {
uploaded: true,
fileName,
originalName: file.name,
size: file.size
};
});
// 404 handler
router.get('/*tag', async ({ params }) => {
return new Response(JSON.stringify({
error: 'Not found',
path: params.tag,
availableEndpoints: ['/health', '/api/users', '/api/files/upload']
}), {
status: 404,
headers: { 'Content-Type': 'application/json' }
});
});
After deploying your worker, test your endpoints:
// Test your worker endpoints
const workerUrl = 'https://your-worker.puter.work';
// Test GET endpoint
const response = await puter.workers.exec(`${workerUrl}/api/hello`);
const data = await response.json();
console.log(data);
// Test POST endpoint
const postResponse = await puter.workers.exec(`${workerUrl}/api/data`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key: 'test', value: 'hello' })
});
const postData = await postResponse.json();
console.log(postData);