Writing OFS Functions
Project structure
my-function/
├── aerostack.json # OFS manifest (required)
├── src/
│ └── index.ts # Function entry point
├── tests/
│ └── index.test.ts # Optional tests
└── package.jsonFunction signature
export default async function(
inputs: Record<string, any>, // Validated against your manifest's input schema
context: {
sdk: AerostackSDK, // Injected by the runtime
user?: User, // Present when auth: "required" or "optional"
env: Env, // Runtime environment variables
meta: {
projectId: string,
functionName: string,
}
}
): Promise<Record<string, any>> // Validated against output schemaExample: user profile function
// aerostack.json
{
"name": "get-user-profile",
"version": "1.0.0",
"description": "Get a user's profile with cached lookups",
"runtime": "any",
"inputs": {
"userId": { "type": "string", "required": true }
},
"outputs": {
"user": { "type": "object", "required": true },
"fromCache": { "type": "boolean" }
},
"requires": ["db", "cache"],
"auth": "required"
}// src/index.ts
export default async function({ userId }, { sdk, user }) {
// Only return your own profile (or admin can see any)
if (userId !== user.id && !user.customFields?.isAdmin) {
throw new Error('Forbidden')
}
const cacheKey = `user:${userId}`
const cached = await sdk.cache.get(cacheKey)
if (cached) return { user: cached, fromCache: true }
const profile = await sdk.db.queryOne(
'SELECT id, email, name, avatar_url, created_at FROM users WHERE id = ?',
[userId]
)
if (!profile) throw new Error('User not found')
await sdk.cache.set(cacheKey, profile, { ttl: 300 })
return { user: profile, fromCache: false }
}Input validation
OFS validates inputs against the manifest schema before your function runs. Invalid inputs throw a 400 VALIDATION_ERROR before your code executes:
"inputs": {
"email": { "type": "string", "required": true, "format": "email" },
"age": { "type": "number", "min": 0, "max": 150 },
"role": { "type": "string", "enum": ["admin", "user", "viewer"] },
"tags": { "type": "array", "items": { "type": "string" } }
}Error handling
Throw plain errors — OFS wraps them in a standard response:
throw new Error('User not found')
// → 404 { "error": "USER_NOT_FOUND", "message": "User not found" }
throw Object.assign(new Error('Rate limit exceeded'), { code: 'RATE_LIMITED', status: 429 })
// → 429 { "error": "RATE_LIMITED", "message": "Rate limit exceeded" }Testing locally
// tests/index.test.ts
import { testFunction } from '@aerostack/test-utils'
import fn from '../src/index'
test('returns user profile', async () => {
const result = await testFunction(fn, {
inputs: { userId: 'test-user-id' },
mockSDK: {
db: {
queryOne: () => ({ id: 'test-user-id', email: '[email protected]' })
},
cache: { get: () => null, set: () => {} }
},
user: { id: 'test-user-id' }
})
expect(result.user.email).toBe('[email protected]')
expect(result.fromCache).toBe(false)
})Run locally
aerostack dev
# → Starts a local OFS runner on http://localhost:8787
# → POST http://localhost:8787/ with JSON body to invoke your function