A library that auto-generates CDK code from TypeSpec API definitions. Used together with faas-stack for type-safe Lambda deployment.
npm install faas-spec// typespec/faas/main.tsp
import "@typespec/http";
import "faas-spec";
using TypeSpec.Http;
using FaasSuite.FaasSpec;
@service(#{ title: "faas-my-service" })
@route("/my-service")
namespace MyService;
@post
@route("/vendors/{vendorId}/signup")
op BuyerSignUp(@path vendorId: string, @body body: SignUpRequest): SignUpResponse;npm run tsp # Compile TypeSpecimport * as APIs from './faas-spec'
export class LambdaStack extends FaasStack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props, SERVICE, STAGE)
APIs.BuyerSignUp(this, envs()) // Type-safe!
}
}Before: Error-prone
// Typo 1: Missing hyphen in filename
this.post('/vendors/{vendorId}/signup', 'buyer-signup.ts', envs())
// Actual file: buyer-sign-up.ts → Runtime error
// Typo 2: Path typo
this.post('/vendors/{vendorId}/signpu', 'buyer-sign-up.ts', envs())
// signpu vs signup → API call failsAfter: TypeSpec ensures consistency
@post
@route("/vendors/{vendorId}/signup")
op BuyerSignUp(@path vendorId: string, ...): ...;// Auto-generated - typos impossible
export const BuyerSignUp = defineSpec(
'BuyerSignUp',
'buyer-sign-up.ts', // Auto-generated from operation name
'post',
'/vendors/{vendorId}/signup',
'node'
)Before: Method mistakes
// Registered as POST when it should be GET
this.post('/products/{id}', 'get-product.ts', envs())After: TypeSpec decorators determine method
@get
@route("/products/{id}")
op GetProduct(@path id: string): Product;// Correct method used automatically
export const GetProduct = defineSpec('GetProduct', 'get-product.ts', 'get', ...)Before: Errors discovered after deployment
this.post('/path', 'non-existent-file.ts', envs())
// CDK synth succeeds, deployment failsAfter: Error at CDK synth time
APIs.NonExistentOperation(this)
// Error: File "./src/non-existent-operation.ts" not found by "NonExistentOperation"
// Discovered immediately before deployment!Before: No autocomplete for strings
this.post('/vendors/{vendorId}/signup', 'buyer-sign-up.ts', envs())After: Autocomplete works for functions
APIs. // ← Ctrl+Space shows all available APIsAPIs.BuyerSignUp(this, envs())
// Access created function
APIs.BuyerSignUp.func // IFunction
APIs.BuyerSignUp.filename // 'buyer-sign-up.ts'
// Usage example
const topic = this.findTopic('notifications')
topic.grantPublish(APIs.BuyerSignUp.func!)// SQS queue subscription
@event("sqs")
op OnSqsOrderCreated(@body body: OrderEvent): void;
// → Queue name: faas-{service}-order-created
// Kafka topic subscription (future support)
@event("kafka")
op OnKafkaOrderChange(@body body: OrderEvent): void;// Cron expression
@schedule
@cron(#{ hour: "20", minute: "15" }) // Daily at 20:15 UTC
op OnScheduleCleanup(): {};
// Rate-based
@schedule
@rate("1h") // Every hour
op OnScheduleHourly(): {};
@schedule
@rate("1d") // Every day
op OnScheduleDaily(): {};
@schedule
@rate("5m") // Every 5 minutes
op OnScheduleFrequent(): {};Rate units:
| Unit | Description | Example |
|---|---|---|
| m | Minutes | "5m" = 5 minutes |
| h | Hours | "1h" = 1 hour |
| d | Days | "1d" = 1 day |
// Default: Node.js (TypeScript)
@get
@route("/products")
op GetProducts(): Product[];
// → File: src/get-products.ts
// Go language
@lang("go")
@get
@route("/hello")
op Hello(): HelloResponse;
// → File: go/hello/main.go| Operation | Filename (Node.js) | Filename (Go) |
|---|---|---|
| GetProducts | get-products.ts | get-products/main.go |
| BuyerSignUp | buyer-sign-up.ts | buyer-sign-up/main.go |
| OnSqsOrderCreated | on-sqs-order-created.ts | - |
| OnScheduleDaily | on-schedule-daily.ts | - |
Conversion rule: PascalCase → kebab-case
@event("sqs")
op OnSqsOrderCreated(...): void;
// ~~~~~~~~~~~~
// Queue name extracted from this part: order-createdActual queue ARN: arn:aws:sqs:{region}:{accountId}:faas-{service}-order-created
// typespec/faas/main.tsp
import "@typespec/http";
import "faas-spec";
using TypeSpec.Http;
using FaasSuite.FaasSpec;
@service(#{ title: "faas-order" })
@route("/order")
namespace Order;
// HTTP APIs
@get @route("/vendors/{vendorId}/orders")
op ListOrders(@path vendorId: string): Order[];
@post @route("/vendors/{vendorId}/orders")
op CreateOrder(@path vendorId: string, @body body: CreateOrderRequest): Order;
// Event Handler
@event("sqs")
op OnSqsOrderCreated(@body body: OrderCreatedEvent): void;
// Scheduled Tasks
@schedule @cron(#{ hour: "2", minute: "0" })
op OnScheduleOrderCleanup(): {};
@schedule @rate("1h")
op OnScheduleOrderStats(): {};// GET /order/vendors/{vendorId}/orders
export const ListOrders = defineSpec('ListOrders', 'list-orders.ts', 'get', '/vendors/{vendorId}/orders', 'node')
// POST /order/vendors/{vendorId}/orders
export const CreateOrder = defineSpec('CreateOrder', 'create-order.ts', 'post', '/vendors/{vendorId}/orders', 'node')
// EVENT(sqs) - faas-order-order-created
export const OnSqsOrderCreated = defineSqsSpec('OnSqsOrderCreated', 'on-sqs-order-created.ts', 'order-created')
// SCHEDULE - order-cleanup
export const OnScheduleOrderCleanup = defineScheduleSpec('OnScheduleOrderCleanup', 'on-schedule-order-cleanup.ts', Schedule.cron({"minute":"0","hour":"2"}))
// SCHEDULE - order-stats
export const OnScheduleOrderStats = defineScheduleSpec('OnScheduleOrderStats', 'on-schedule-order-stats.ts', Schedule.rate(Duration.hours(1)))import * as APIs from './faas-spec'
export class LambdaStack extends FaasStack {
constructor(scope: App, id: string, props?: StackProps) {
super(scope, id, props, SERVICE, STAGE, { shareFaasSecurityGroup: true, awsv3: true })
// External API
APIs.CreateOrder(this, envs())
// Internal API
this.switchToPrivateGateway()
APIs.ListOrders(this, envs())
// Event & Schedule
APIs.OnSqsOrderCreated(this, envs())
APIs.OnScheduleOrderCleanup(this, envs())
APIs.OnScheduleOrderStats(this, envs())
// Grant permissions
const table = this.findTable('Orders')
for (const name in this.functionCache) {
table.grantReadWriteData(this.functionCache[name])
}
}
}faas-spec validates the following at TypeSpec compile time:
| Error Code | Description |
|---|---|
event-before-route |
@event must be used after @route |
invalid-route-path |
@event route must start with /events/{type} |
invalid-operation-name |
@event operation name must start with On{Type} |
schedule-requires-cron-or-rate |
@schedule requires @cron or @rate |
invalid-rate-unit |
Rate unit must be m/h/d |