sam-goodwin / punchcard

Type-safe AWS infrastructure.
Apache License 2.0
507 stars 20 forks source link

[WIP] (feat: api-gateway): Smithy-like syntax and Velocity-Template DSL for Api Gatewau #111

Closed sam-goodwin closed 4 years ago

sam-goodwin commented 4 years ago

Excited with where this change is going! A total re-work of the API Gateway stuff and preparation for AppSync/GraphQL.

Closes #110

The idea is to entirely abstract VTL transformation done by API Gateway as ordinary function calls and data operations. Let's look at an example.

First, define some record types.

class GetUserRequest extends Record({
  userId: string
}) {}
class GetUserResponse extends Record({
  userId: string,
  userName: string
}) {}

Create a Service and an Endpoint:

// endpoint hosted in a Lambda Function
const endpoint = new Api.Endpoint(..);

const GameService = new Api.Service({
  serviceName: 'game-service',
});

Create a callable API hosted in our Lambda Endpoint.

const GetUserHandler = new Api.Call({
  endpoint,
  input: GetUserRequest,
  output: GetUserResponse
}, async request => Ok(new GetUserResponse({
  userId: request.userId,
  userName: 'user name'
})));

Add an "Operation" to the service (static function call, not resourceful yet):

GameService.addOperation('GetUser', GetUserRequest,
  request => GetUserHandler.call(request));

This operation accepts a GetUserRequest and returns a GetUserResponse by proxying the call to the Lambda Function.

What if the lambda only accepted a string instead of a GetUserRequest? We can express VTL transformation in a very simple, type-safe way:

GameService.addOperation('GetUser', GetUserRequest,
  // request.userId will synthesize a VTL transformation that extracts the userId key from the JSON payload received as a body
  request => GetUserHandler.call(request.userId));

Same is true for the output of an API integration:

GameService.addOperation('GetUser', GetUserRequest,
  // this time, both the input and output is transformed with VTL.
  // the output template will extract the userName field from the GetUserResponse returned by the Lambda integration
  request => GetUserHandler.call(request.userId).userName);

Where this really starts to shine is with service proxies like DynamoDB or SQS:

const queue = new SQS.Queue(..);
GameService.addOperation('SendMessage', string, s => queue.sendMessage(s));

const table = new DynamoDB.Table(..);
GameService.addOperation('PutItem', string, s => table.put(VTL.Record({
  id: s
}));

So far, we've only configured static operations on the API. Taking inspiration from Smithy, you can also add resources to the service:

const User = GameService.addResource({
  name: 'user',
  identifiers: {
    userId: string
  }
});

And then configure operations for get, create, update and list:

User.onGet(GetUserRequest, request => GetUserHandler.call(request));