Service
Service
Overview
Services are stateless HTTP workers that handle requests and responses without maintaining persistent state between invocations. Each service processes requests independently and can be deployed as public endpoints or internal bindings for inter-service communication.
Services excel at API development, webhook handling, and request routing where each invocation should be independent. They receive fresh execution contexts for every request, making them suitable for stateless operations that don’t require persistent memory.
Key benefits include stateless execution model, automatic request routing, inter-service communication through bindings, and public URL access through domain configuration.
Prerequisites
- Active Raindrop project with manifest configuration
- Understanding of TypeScript classes and HTTP request/response patterns
- Familiarity with serverless function concepts
- Knowledge of REST API design principles
Creating/Getting Started
Define services in your manifest file to create HTTP request handlers:
application "api-app" { service "user-api" { visibility = "public" domain { fqdn = "api.example.com" } }
service "subdomain-api" { visibility = "public" domain { cname = "my-api-v2" } }
service "internal-processor" { visibility = "application" }}
Generate the service implementation:
raindrop build generate
This creates the base service class structure:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { return new Response('Hello World'); }}
Accessing/Basic Usage
Implement HTTP request handling in the fetch
method with full access to request data:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const method = request.method;
if (method === 'GET' && url.pathname === '/users') { return this.getUsers(); }
if (method === 'POST' && url.pathname === '/users') { const userData = await request.json(); return this.createUser(userData); }
return new Response('Not Found', { status: 404 }); }
private async getUsers(): Promise<Response> { const users = [{ id: 1, name: 'John Doe' }]; return Response.json(users); }
private async createUser(userData: any): Promise<Response> { const newUser = { id: Date.now(), ...userData }; return Response.json(newUser, { status: 201 }); }}
Access other services through environment bindings:
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { // Call internal service method const result = await this.env.INTERNAL_PROCESSOR.process(data); return Response.json(result); }}
Core Concepts
Services follow a stateless execution model where each request receives a fresh execution context without persistent state between invocations.
Execution Context: Each service instance receives an ExecutionContext
providing access to runtime capabilities and the waitUntil
method for background tasks.
Environment Bindings: Services access other resources through the env
property containing typed bindings to actors, databases, storage, and other services.
Request Lifecycle: Services process individual HTTP requests through the fetch
method, with automatic cleanup after each response.
Service Stubs: Internal services are accessed through typed stubs that provide RPC-style method calls with Promise-wrapped return values.
Service Base Class
The Service<Env>
abstract class provides the foundation for all service implementations:
abstract class Service<Env> { ctx: ExecutionContext; env: Env;
constructor(ctx: ExecutionContext, env: Env) { this.ctx = ctx; this.env = env; }
abstract fetch(request: Request): Promise<Response>;}
Properties:
ctx
:ExecutionContext
- Runtime execution contextenv
:Env
- Typed environment bindings for resources
Request Handling
Process HTTP requests using the standard Web API Request/Response interfaces with full access to headers, body, and URL parameters.
fetch(request)
Handle incoming HTTP requests with complete request information:
async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const headers = request.headers; const method = request.method;
// Parse request body for POST/PUT requests let body = null; if (method === 'POST' || method === 'PUT') { body = await request.json(); }
// Route based on path and method switch (`${method} ${url.pathname}`) { case 'GET /health': return new Response('OK');
case 'POST /webhook': return this.handleWebhook(body, headers);
case 'GET /api/users': const limit = url.searchParams.get('limit') || '10'; return this.getUsers(parseInt(limit));
default: return new Response('Not Found', { status: 404 }); }}
Parameters:
request
:Request
- Standard Web API Request object
Returns: Promise<Response>
- Standard Web API Response object
Request Processing Patterns
Handle common HTTP patterns with proper error handling and response formatting:
// JSON API responsesprivate async handleApiRequest(request: Request): Promise<Response> { try { const data = await this.processRequest(request); return Response.json({ success: true, data }); } catch (error) { return Response.json({ success: false, error: error.message }, { status: 500 }); }}
// File upload handlingprivate async handleFileUpload(request: Request): Promise<Response> { const formData = await request.formData(); const file = formData.get('file') as File;
if (!file) { return new Response('No file provided', { status: 400 }); }
const buffer = await file.arrayBuffer(); // Process file buffer...
return Response.json({ filename: file.name, size: buffer.byteLength });}
// Webhook signature verificationprivate async verifyWebhook(request: Request): Promise<boolean> { const signature = request.headers.get('X-Signature'); const body = await request.text();
// Verify signature logic... return signature === expectedSignature;}
Visibility Configuration
Configure service access levels using the visibility
parameter to control deployment behavior and routing.
Visibility Options
Services support these visibility levels:
public
: Accessible on the internet without authenticationprotected
: Requires authentication via authorization serverprivate
: Accessible within private network onlyapplication
: Internal to application, accessible via service bindingsnone
: No external routing, internal use onlysuite
: Suite-level access (specialized use)tenant
: Tenant-specific access (specialized use)
Development Best Practices
Use visibility = "public"
during development to receive generated URLs:
service "dev-api" { visibility = "public" # Check generated URL with: raindrop build status}
Check the generated URL after deployment:
raindrop build status
This approach avoids CNAME binding conflicts that cause deployment issues.
Production Configuration
Reserve custom domains for production services with static address requirements:
service "prod-api" { visibility = "public" domain { fqdn = "api.mycompany.com" }}
Domain Configuration
Services can be configured with custom domains for external access using either fully qualified domain names (FQDN) or auto-generated subdomains (CNAME).
Domain Block Structure
Define domains within a domain
block in your service configuration:
service "my-service" { domain { fqdn = "api.mycompany.com" # Custom domain }}
# OR
service "my-service" { domain { cname = "my-unique-subdomain" # Auto-generated subdomain }}
Configuration Requirements:
- Each
domain
block must specify eithercname
ORfqdn
(but not both) fqdn
: Fully qualified domain name (e.g., “api.example.com”)cname
: Subdomain prefix that generates “<cname>.<org-id>.lmapp.run
”
Domain Validation Rules
Domain configurations are validated with these constraints:
FQDN Validation:
- Must be a valid domain name format
- Maximum length: 63 characters
- Pattern:
/^[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,6}$/
CNAME Validation:
- Must be a valid subdomain prefix
- Maximum length: ~35 characters (63 minus platform suffix)
- Pattern validates as subdomain format
Domain Configuration Examples
application "multi-domain-app" { # Production API with custom domain service "api" { visibility = "public" domain { fqdn = "api.mycompany.com" } }
# Development API with auto-generated subdomain service "dev-api" { visibility = "public" domain { cname = "dev-api-v2" } # Results in: dev-api-v2.org_abc123.lmapp.run }
# Multiple services with different domains service "admin" { visibility = "protected" domain { fqdn = "admin.mycompany.com" } }
service "webhooks" { visibility = "public" domain { cname = "webhooks-prod" } }}
Service Bindings
Call methods on other services within your application using typed stubs that provide RPC-style communication.
Internal Service Communication
Services without domain configuration are internal and accessed through bindings:
application "microservices" { service "api-gateway" { visibility = "public" domain { fqdn = "api.example.com" } }
service "user-service" { visibility = "application" } service "order-service" { visibility = "application" }}
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname.startsWith('/users')) { return this.env.USER_SERVICE.handleUserRequest(request); }
if (url.pathname.startsWith('/orders')) { const userId = url.searchParams.get('userId'); return this.env.ORDER_SERVICE.getUserOrders(userId); }
return new Response('Not Found', { status: 404 }); }}
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { return new Response('Internal service', { status: 501 }); }
async handleUserRequest(request: Request): Promise<Response> { const url = new URL(request.url); const userId = url.pathname.split('/users/')[1];
const user = await this.getUserById(userId); return Response.json(user); }
async getUserById(id: string): Promise<any> { return { id, name: `User ${id}`, email: `user${id}@example.com` }; }}
Key Requirements:
- All service binding calls must be awaited
- Internal services still require a
fetch
method implementation - Custom methods can be called through bindings, but not
fetch
- Return values are automatically wrapped in Promises
ServiceStub Type
Service bindings are typed as ServiceStub<T>
providing type safety for method calls:
type ServiceStub<T extends Service<Env>, Env = unknown> = Stub<T>;
// Stub converts methods to Promise-wrapped versionstype Stub<T> = { [P in keyof T as T[P] extends (...args: any[]) => any ? P : never]: T[P] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : never;};
Background Tasks
Use the execution context to schedule background work that continues after response is sent.
ctx.waitUntil(promise)
Schedule asynchronous work to complete after the response:
async fetch(request: Request): Promise<Response> { // Send immediate response const response = Response.json({ status: 'received' });
// Schedule background processing this.ctx.waitUntil( this.processAsync(request) );
return response;}
private async processAsync(request: Request): Promise<void> { // This continues running after response is sent const data = await request.json(); await this.sendNotification(data); await this.updateAnalytics(data);}
Parameters:
promise
:Promise<any>
- Asynchronous work to complete
Code Examples
Complete service implementations demonstrating common patterns and use cases.
REST API Service
application "todo-api" { service "api" { visibility = "public" domain { fqdn = "todos.example.com" } }
sql_database "todos" {}}
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url); const method = request.method;
// Enable CORS for browser requests const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE', 'Access-Control-Allow-Headers': 'Content-Type' };
if (method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); }
try { const response = await this.routeRequest(method, url);
// Add CORS headers to all responses Object.entries(corsHeaders).forEach(([key, value]) => { response.headers.set(key, value); });
return response; } catch (error) { return Response.json( { error: error.message }, { status: 500, headers: corsHeaders } ); } }
private async routeRequest(method: string, url: URL): Promise<Response> { const path = url.pathname; const id = path.split('/todos/')[1];
switch (`${method} ${path}`) { case 'GET /todos': return this.getAllTodos();
case 'POST /todos': return this.createTodo(request);
case `GET /todos/${id}`: return this.getTodo(id);
case `PUT /todos/${id}`: return this.updateTodo(id, request);
case `DELETE /todos/${id}`: return this.deleteTodo(id);
default: return new Response('Not Found', { status: 404 }); } }
private async getAllTodos(): Promise<Response> { const result = await this.env.TODOS.prepare( 'SELECT * FROM todos ORDER BY created_at DESC' ).all();
return Response.json(result.results); }
private async createTodo(request: Request): Promise<Response> { const { title, description } = await request.json();
if (!title) { return Response.json( { error: 'Title is required' }, { status: 400 } ); }
const result = await this.env.TODOS.prepare( 'INSERT INTO todos (title, description, completed) VALUES (?, ?, ?) RETURNING *' ).bind(title, description, false).first();
return Response.json(result, { status: 201 }); }
private async getTodo(id: string): Promise<Response> { const todo = await this.env.TODOS.prepare( 'SELECT * FROM todos WHERE id = ?' ).bind(id).first();
if (!todo) { return new Response('Todo not found', { status: 404 }); }
return Response.json(todo); }
private async updateTodo(id: string, request: Request): Promise<Response> { const updates = await request.json();
const result = await this.env.TODOS.prepare( 'UPDATE todos SET title = ?, description = ?, completed = ? WHERE id = ? RETURNING *' ).bind( updates.title, updates.description, updates.completed, id ).first();
if (!result) { return new Response('Todo not found', { status: 404 }); }
return Response.json(result); }
private async deleteTodo(id: string): Promise<Response> { const result = await this.env.TODOS.prepare( 'DELETE FROM todos WHERE id = ? RETURNING id' ).bind(id).first();
if (!result) { return new Response('Todo not found', { status: 404 }); }
return Response.json({ deleted: true }); }}
Webhook Processing Service
export default class extends Service<Env> { async fetch(request: Request): Promise<Response> { const url = new URL(request.url);
if (url.pathname === '/webhook/stripe') { return this.handleStripeWebhook(request); }
if (url.pathname === '/webhook/github') { return this.handleGitHubWebhook(request); }
return new Response('Unknown webhook', { status: 404 }); }
private async handleStripeWebhook(request: Request): Promise<Response> { const signature = request.headers.get('stripe-signature'); const body = await request.text();
// Verify webhook signature if (!this.verifyStripeSignature(body, signature)) { return new Response('Invalid signature', { status: 401 }); }
const event = JSON.parse(body);
// Process webhook asynchronously this.ctx.waitUntil( this.processStripeEvent(event) );
return Response.json({ received: true }); }
private async processStripeEvent(event: any): Promise<void> { switch (event.type) { case 'payment_intent.succeeded': await this.handlePaymentSuccess(event.data.object); break;
case 'customer.subscription.deleted': await this.handleSubscriptionCanceled(event.data.object); break;
default: console.log(`Unhandled event type: ${event.type}`); } }
private async handlePaymentSuccess(paymentIntent: any): Promise<void> { // Update database, send notifications, etc. await this.env.ORDERS.prepare( 'UPDATE orders SET status = ? WHERE payment_intent_id = ?' ).bind('paid', paymentIntent.id).run(); }
private verifyStripeSignature(body: string, signature: string | null): boolean { // Implement Stripe webhook signature verification return signature !== null; // Simplified }}
## raindrop.manifest
Configure HTTP services in your manifest for request handling and API endpoints:
```hclapplication "web-app" { service "api" { visibility = "public" domain { fqdn = "api.example.com" } # Public HTTP service accessible via custom domain }
service "admin-api" { visibility = "protected" domain { cname = "admin-v2" } # Admin interface with auto-generated subdomain }
service "webhook-handler" { visibility = "public" domain { fqdn = "webhooks.example.com" } # Service for handling external webhooks }
service "internal-processor" { visibility = "application" # Internal service accessible only via bindings # No domain = not publicly accessible }
actor "data-processor" { # Services can communicate with actors }
kv "session-cache" { # Services can use other resources }}