Testing & Debugging — Functions
Aerostack functions run on Cloudflare Workers, which means you get a full local development environment powered by the local runtime (Miniflare). All six platform bindings work locally — including Database, Cache, Queue, AI, Vector Search, and Storage.
Local Development
Section titled “Local Development”Start the dev server
Section titled “Start the dev server”aerostack devStarts a local development server with hot reload. All bindings are simulated locally. The function is accessible at http://localhost:8787 by default.
What happens locally
Section titled “What happens locally”| Binding | Local behavior |
|---|---|
env.DB | Local SQLite file (persistent between restarts) |
env.CACHE | Local cache store (persistent between restarts) |
env.QUEUE | Local queue simulation (messages delivered immediately) |
env.AI | Requires --remote flag or mocked (see below) |
env.VECTORIZE | Requires --remote flag or mocked |
env.STORAGE | Local storage simulation (files stored in .aerostack/state/) |
Testing with remote bindings
Section titled “Testing with remote bindings”To test AI and Vector Search locally against Cloudflare’s real infrastructure:
aerostack dev --remoteThis runs your function locally but connects to production bindings. Use with caution — writes to the database, cache, and storage will affect real data.
Local data persistence
Section titled “Local data persistence”Local data is stored in .aerostack/state/ inside your project directory:
.aerostack/ state/ v3/ d1/ # Local database files kv/ # Local cache data r2/ # Local storage objectsTo reset local state and start fresh:
rm -rf .aerostack/stateaerostack devDebugging
Section titled “Debugging”Console logging
Section titled “Console logging”The simplest and most effective debugging tool. Logs appear in the terminal where aerostack dev is running.
export default { async fetch(request: Request, env: Env): Promise<Response> { const url = new URL(request.url) console.log(`[${request.method}] ${url.pathname}`)
const start = Date.now() const { results } = await env.DB.prepare('SELECT * FROM users').all() console.log(`DB query took ${Date.now() - start}ms, returned ${results.length} rows`)
return Response.json(results) }}Viewing logs in production
Section titled “Viewing logs in production”For deployed functions, use the Aerostack CLI to stream live logs:
aerostack logs my-function-nameThis streams all console.log, console.error, and exception output from your production function in real-time. Filter by status:
# Only errorsaerostack logs my-function-name --status error
# Only specific pathsaerostack logs my-function-name --search "/api/users"Error handling pattern
Section titled “Error handling pattern”Wrap your handler in a try-catch to ensure errors are always logged and returned as structured JSON:
export default { async fetch(request: Request, env: Env): Promise<Response> { try { return await handleRequest(request, env) } catch (err) { console.error('Unhandled error:', err)
const message = err instanceof Error ? err.message : 'Unknown error' const stack = err instanceof Error ? err.stack : undefined
return Response.json({ error: message, ...(env.ENVIRONMENT === 'development' ? { stack } : {}) }, { status: 500 }) } }}
async function handleRequest(request: Request, env: Env): Promise<Response> { // Your actual logic here — errors bubble up to the catch block}Testing Strategies
Section titled “Testing Strategies”Manual testing with curl
Section titled “Manual testing with curl”The fastest way to test during development:
# GET requestcurl http://localhost:8787/api/users
# POST with JSON bodycurl -X POST http://localhost:8787/api/users \ -H "Content-Type: application/json" \ -d '{"name": "Alice", "email": "alice@example.com"}'
# With headerscurl http://localhost:8787/api/users \ -H "Authorization: Bearer test-token" \ -H "Accept: application/json"
# See response headerscurl -i http://localhost:8787/api/usersUnit testing with Vitest
Section titled “Unit testing with Vitest”Use Vitest with @cloudflare/vitest-pool-workers for unit tests that run against real Cloudflare bindings locally.
Install the test dependencies:
npm install -D vitest @cloudflare/vitest-pool-workersCreate vitest.config.ts:
import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config'
export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: './aerostack.toml' } } } }})Write a test in test/index.test.ts:
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test'import { describe, it, expect, beforeAll } from 'vitest'import worker from '../src/index'
describe('Bookmarks API', () => { beforeAll(async () => { // Set up test data await env.DB.exec(` CREATE TABLE IF NOT EXISTS bookmarks ( id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, title TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')) ) `) })
it('creates a bookmark', async () => { const request = new Request('http://localhost/bookmarks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com', title: 'Example' }) })
const ctx = createExecutionContext() const response = await worker.fetch(request, env, ctx) await waitOnExecutionContext(ctx)
expect(response.status).toBe(201)
const body = await response.json() expect(body.url).toBe('https://example.com') expect(body.title).toBe('Example') expect(body.id).toBeDefined() })
it('lists bookmarks', async () => { const request = new Request('http://localhost/bookmarks') const ctx = createExecutionContext() const response = await worker.fetch(request, env, ctx) await waitOnExecutionContext(ctx)
expect(response.status).toBe(200)
const body = await response.json() expect(Array.isArray(body)).toBe(true) expect(body.length).toBeGreaterThan(0) })
it('returns 400 for missing fields', async () => { const request = new Request('http://localhost/bookmarks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: 'https://example.com' }) // missing title })
const ctx = createExecutionContext() const response = await worker.fetch(request, env, ctx) await waitOnExecutionContext(ctx)
expect(response.status).toBe(400) })})Run tests:
npx vitest runIntegration testing
Section titled “Integration testing”For functions that interact with multiple bindings, test the full flow:
it('caches DB results', async () => { // First request — cache miss const res1 = await worker.fetch(new Request('http://localhost/bookmarks'), env, createExecutionContext()) expect(res1.headers.get('X-Cache')).toBe('MISS')
// Second request — cache hit const res2 = await worker.fetch(new Request('http://localhost/bookmarks'), env, createExecutionContext()) expect(res2.headers.get('X-Cache')).toBe('HIT')
// Verify same data const data1 = await res1.json() const data2 = await res2.json() expect(data1).toEqual(data2)})Common Issues
Section titled “Common Issues””No such table” error
Section titled “”No such table” error”Your database schema has not been applied locally. Run your migration:
aerostack db execute --local --file=schema.sqlAI binding returns “AI is not defined”
Section titled “AI binding returns “AI is not defined””The AI binding requires either:
--remoteflag when runningaerostack dev- The
[ai]section in youraerostack.toml
[ai]binding = "AI"Queue messages not being processed
Section titled “Queue messages not being processed”Ensure your aerostack.toml has both a producer and consumer configuration:
[[queues.producers]]binding = "QUEUE"queue = "my-queue"
[[queues.consumers]]queue = "my-queue"And that your function exports a queue() handler.
Cache returns null for a key that was just written
Section titled “Cache returns null for a key that was just written”The cache is eventually consistent. In production, writes can take up to 60 seconds to propagate globally. For data that must be immediately consistent, use the database instead of the cache.
Locally, the cache is immediately consistent — this issue only appears in production.
Function works locally but fails when deployed
Section titled “Function works locally but fails when deployed”Common causes:
- Missing secrets — set them with
aerostack secrets set KEY - Schema not applied — run migrations on the remote database
- Binding mismatch — ensure
aerostack.tomlbinding names match what your code uses (env.DB,env.CACHE, etc.) - Compatibility date — if using newer APIs, update
compatibility_dateinaerostack.toml