packages/worker-shared/DOCS.md
The @tldraw/worker-shared package provides common utilities and handlers for Cloudflare Workers used across the tldraw infrastructure. It handles API routing, error responses, asset management, bookmark metadata extraction, and Sentry integration for worker environments.
This package contains shared functionality designed specifically for Cloudflare Workers. It provides a structured approach to building APIs with proper error handling, request routing, and asset management capabilities. The utilities are designed to work seamlessly with Cloudflare's R2 storage, Sentry error tracking, and the Worker runtime environment.
Key capabilities:
itty-routerThe package provides utilities for creating robust API handlers with proper error handling and routing.
import { createRouter, handleApiRequest, parseRequestQuery } from '@tldraw/worker-shared'
import { T } from '@tldraw/validate'
// Create a type-safe router
const router = createRouter<Env, ExecutionContext>()
// Add routes with typed handlers
router.get('/api/health', () => {
return Response.json({ status: 'ok' })
})
// Handle requests with automatic error handling
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return handleApiRequest({
router,
request,
env,
ctx,
after: (response) => {
// Add CORS headers or other post-processing
response.headers.set('Access-Control-Allow-Origin', '*')
return response
},
})
},
}
The handleApiRequest function automatically:
after callbackParse and validate request parameters using tldraw's validation system:
import { parseRequestQuery } from '@tldraw/worker-shared'
import { T } from '@tldraw/validate'
const queryValidator = T.object({
url: T.httpUrl,
limit: T.number.optional(),
})
router.get('/api/process', (request) => {
// Automatically validates and throws 400 errors for invalid input
const { url, limit } = parseRequestQuery(request, queryValidator)
console.log(url) // Guaranteed to be a valid HTTP URL
console.log(limit) // number | undefined
return Response.json({ processed: url })
})
The package provides consistent error response utilities:
import { notFound, forbidden } from '@tldraw/worker-shared'
router.get('/api/resource/:id', (request) => {
const { id } = request.params
if (!hasPermission(id)) {
return forbidden() // Returns 403 with standard error format
}
const resource = getResource(id)
if (!resource) {
return notFound() // Returns 404 with standard error format
}
return Response.json(resource)
})
Both functions return standardized JSON error responses:
forbidden(): { "error": "Forbidden" } with status 403notFound(): { "error": "Not found" } with status 404Error tracking is automatically configured when environment variables are present:
import { createSentry } from '@tldraw/worker-shared'
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Sentry is automatically initialized in handleApiRequest
// Or create manually for custom error handling
const sentry = createSentry(ctx, env, request)
try {
// Your worker logic
} catch (error) {
sentry?.captureException(error)
throw error
}
},
}
The Sentry integration requires these environment variables:
SENTRY_DSN: Your Sentry project DSNWORKER_NAME: Name of the worker for release trackingCF_VERSION_METADATA: Cloudflare version metadata for releasesHandle asset uploads to Cloudflare R2 with conflict detection:
import { handleUserAssetUpload } from '@tldraw/worker-shared'
router.put('/assets/:objectName', async (request, env) => {
const { objectName } = request.params
return handleUserAssetUpload({
objectName,
bucket: env.ASSETS_BUCKET, // Your R2 bucket binding
body: request.body,
headers: request.headers,
})
})
The upload handler:
Serve assets with automatic caching and range request support:
import { handleUserAssetGet } from '@tldraw/worker-shared'
router.get('/assets/:objectName', async (request, env, ctx) => {
const { objectName } = request.params
return handleUserAssetGet({
request,
bucket: env.ASSETS_BUCKET,
objectName,
context: ctx,
})
})
The retrieval handler provides:
Extract metadata from web pages for bookmark creation:
import { handleExtractBookmarkMetadataRequest } from '@tldraw/worker-shared'
router.get('/api/bookmark', (request) => {
return handleExtractBookmarkMetadataRequest({ request })
})
// Usage: GET /api/bookmark?url=https://example.com
// Returns: { title: "Example", description: "...", image: "...", favicon: "..." }
For production use, provide image uploading to store optimized versions:
router.post('/api/bookmark', (request) => {
return handleExtractBookmarkMetadataRequest({
request,
uploadImage: async (headers, body, objectName) => {
// Store the processed image and return its URL
await env.ASSETS_BUCKET.put(objectName, body, { httpMetadata: headers })
return `https://assets.example.com/${objectName}`
},
})
})
When uploadImage is provided, the handler:
Use the requiredEnv utility to validate required environment variables:
import { requiredEnv } from '@tldraw/worker-shared'
export interface Env {
readonly DATABASE_URL?: string
readonly API_KEY?: string
readonly ASSETS_BUCKET: R2Bucket
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
// Throws clear error if variables are missing
const config = requiredEnv(env, {
DATABASE_URL: true,
API_KEY: true,
})
// config.DATABASE_URL and config.API_KEY are guaranteed non-null
console.log(`Connecting to ${config.DATABASE_URL}`)
},
}
Here's a complete worker using all the shared utilities:
import {
createRouter,
handleApiRequest,
handleUserAssetGet,
handleUserAssetUpload,
handleExtractBookmarkMetadataRequest,
notFound,
} from '@tldraw/worker-shared'
export interface Env {
readonly ASSETS_BUCKET: R2Bucket
readonly SENTRY_DSN?: string
readonly WORKER_NAME?: string
}
const router = createRouter<Env, ExecutionContext>()
// Asset management
router.put('/assets/:objectName', async (request, env) => {
const { objectName } = request.params
return handleUserAssetUpload({
objectName,
bucket: env.ASSETS_BUCKET,
body: request.body,
headers: request.headers,
})
})
router.get('/assets/:objectName', async (request, env, ctx) => {
const { objectName } = request.params
return handleUserAssetGet({
request,
bucket: env.ASSETS_BUCKET,
objectName,
context: ctx,
})
})
// Bookmark extraction
router.post('/api/bookmark', (request) => {
return handleExtractBookmarkMetadataRequest({
request,
uploadImage: async (headers, body, objectName) => {
await env.ASSETS_BUCKET.put(objectName, body, { httpMetadata: headers })
return `https://assets.example.com/${objectName}`
},
})
})
// Fallback
router.all('*', () => notFound())
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
return handleApiRequest({
router,
request,
env,
ctx,
after: (response) => {
response.headers.set('Access-Control-Allow-Origin', '*')
return response
},
})
},
}
The package is built with TypeScript and provides full type safety:
import { ApiRouter, ApiRoute } from '@tldraw/worker-shared'
// Router type includes environment and context constraints
const router: ApiRouter<MyEnv, ExecutionContext> = createRouter()
// Route type ensures handlers match the expected signature
const getRoute: ApiRoute<MyEnv, ExecutionContext> = router.get
Environment interfaces can extend SentryEnvironment for automatic Sentry integration:
import { SentryEnvironment } from '@tldraw/worker-shared'
interface MyEnv extends SentryEnvironment {
readonly DATABASE_URL: string
readonly ASSETS_BUCKET: R2Bucket
}
This ensures your environment has the necessary Sentry configuration properties while maintaining type safety for your custom variables.
Note: All handlers are designed to work with Cloudflare Workers' execution model and integrate seamlessly with the Worker runtime environment.