@aws-prototyping-sdk/type-safe-api
Project description
Type Safe API
Define your APIs using Smithy or OpenAPI v3, and leverage the power of generated client and server types, infrastructure, documentation, and automatic input validation!
This package vends a projen project type which allows you to define an API using either Smithy or OpenAPI v3, and a construct which manages deploying this API in API Gateway, given an integration (eg a lambda) for every operation.
The project will generate "runtime" projects from your API definition in your desired languages, which can be utilised both client side for interacting with your API, or server side for implementing your API. The project also generates a type-safe CDK construct which ensures an integration is provided for every API operation.
Code is generated at build time, so when you change your API model, just rebuild and you'll see your changes reflected in the generated code.
Quick Start: TypeScript
This section describes how to get started quickly, with TypeScript infrastructure and lambda handlers. See the end of the README for examples in Python and Java.
Create Your API Project
Use the project in your .projenrc.ts
. It can either be part of an nx-monorepo
(recommended) or used in a standalone fashion.
import { NxMonorepoProject } from "@aws-prototyping-sdk/nx-monorepo";
import { TypeSafeApiProject } from "@aws-prototyping-sdk/type-safe-api";
import { AwsCdkTypeScriptApp } from "projen/lib/awscdk";
// Create the monorepo
const monorepo = new NxMonorepoProject({ ... });
// Create the API project
const api = new TypeSafeApiProject({
name: "myapi",
parent: monorepo,
outdir: 'packages/api',
// Smithy as the model language. You can also use ModelLanguage.OPENAPI
model: {
language: ModelLanguage.SMITHY,
options: {
smithy: {
serviceName: {
namespace: 'com.mycompany',
serviceName: 'MyApi',
},
},
},
},
// Generate types, client and server code in TypeScript, Python and Java
runtime: {
languages: [Language.TYPESCRIPT, Language.PYTHON, Language.JAVA],
},
// CDK infrastructure in TypeScript
infrastructure: {
language: Language.TYPESCRIPT,
},
// Generate HTML documentation
documentation: {
formats: [DocumentationFormat.HTML_REDOC],
}
});
// Create a CDK infrastructure project
const infra = new AwsCdkTypeScriptApp({ ... });
// Infrastructure can depend on the generated API infrastructure and runtime
infra.addDeps(api.infrastructure.typescript!.package.packageName);
infra.addDeps(api.runtime.typescript!.package.packageName);
monorepo.synth();
Use the CDK Construct
In your CDK application, consume the Api
construct, vended from the generated typescript infrastructure package.
import { Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Api } from "myapi-typescript-infra"; // <- generated typescript infrastructure package
import { Authorizers, Integrations } from "@aws-prototyping-sdk/type-safe-api";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Cors } from "aws-cdk-lib/aws-apigateway";
import * as path from 'path';
export class MyStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
// Instantiate the generated CDK construct to deploy an API Gateway API based on your model
new Api(this, 'MyApi', {
defaultAuthorizer: Authorizers.iam(),
corsOptions: {
allowOrigins: Cors.ALL_ORIGINS,
allowMethods: Cors.ALL_METHODS,
},
// Supply an integration for every operation
integrations: {
sayHello: {
integration: Integrations.lambda(new NodejsFunction(this, 'SayHelloLambda', {
entry: path.resolve(__dirname, 'say-hello.ts'),
})),
},
},
});
}
}
Implement a Lambda Handler
The generated runtime projects include lambda handler wrappers which provide type-safety for implementing your API operations. You can implement your lambda handlers in any of the supported languages, and even mix and match languages for different operations if you like. In typescript, the implementation of say-hello.ts
would look like:
import { sayHelloHandler } from "myapi-typescript-runtime"; // <- generated typescript runtime package
// Use the handler wrapper for type-safety to ensure you correctly implement your modelled API operation
export const handler = sayHelloHandler(async ({ input }) => {
return {
statusCode: 200,
body: {
message: `Hello ${input.requestParameters.name}`,
},
};
});
Project
The TypeSafeApiProject
projen project sets up the project structure for you. You have a few parameters to consider when creating the project:
model
- Configure the API model. Select alanguage
for the model of either Smithy or OpenAPI v3, and supplyoptions.smithy
oroptions.openapi
depending on your choice.runtime
- Configure the generated runtime projects. Include one or morelanguages
you wish to write your client and server-side code in. These projects contain generated types defined in your model, as well as type-safe lambda handler wrappers for implementing each operation.infrastructure
- Pick thelanguage
you are writing your CDK infrastructure in. A construct will be generated in this language which can be used to deploy the API.documentation
- Specifyformats
to generate documentation in.
It's recommended that these projects are used as part of an nx-monorepo
project (eg. by specifying parent: myMonorepoProject
), as it makes setting up dependencies much easier, particularly when extending your project further with a CDK app and lambda functions.
Depending on the model.language
you choose, you must supply the corresponding model.options
. For example:
new TypeSafeApiProject({
model: {
language: ModelLanguage.SMITHY,
options: {
smithy: {
serviceName: {
namespace: 'com.mycompany',
serviceName: 'MyApi',
},
},
},
},
...
});
new TypeSafeApiProject({
model: {
language: ModelLanguage.OPENAPI,
options: {
openapi: {
title: 'MyApi',
},
},
},
...
});
model.options.smithy
allows for further customisation of the Smithy project, eg:
new TypeSafeApiProject({
model: {
language: ModelLanguage.SMITHY,
options: {
smithy: {
serviceName: {
namespace: 'com.mycompany',
serviceName: 'MyApi',
},
// By default, the contents of the smithy build output directory `model/output` will be ignored by source control.
// Set this to false to include it, for example if you are generating clients directly from the smithy model.
ignoreSmithyBuildOutput: false,
// The gradle wrapper used for the smithy build is copied from the PDK itself if it does not already exist in
// the 'smithy' folder. By default, this gradle wrapper will be ignored by source control.
// Set this to false if you would like to check the gradle wrapper in to source control, for example if you want
// to use a different version of the gradle wrapper in your project.
ignoreGradleWrapper: false,
// Use smithyBuildOptions to control what is added to smithy-build.json.
smithyBuildOptions: {
projections: {
// You can customise the built-in openapi projection, used to generate the OpenAPI specification.
openapi: {
plugins: {
openapi: {
// Customise the openapi projection here.
// See: https://smithy.io/2.0/guides/converting-to-openapi.html
useIntegerType: true,
...
}
}
},
// You can add new projections here too
"ts-client": {
"plugins": {
"typescript-codegen": {
"package" : "@my-test/smithy-generated-typescript-client",
"packageVersion": "0.0.1"
}
}
}
},
// Note that any additional dependencies required for projections/plugins can be added here, which in turn will
// add them to the `smithy/build.gradle` file
maven: {
dependencies: [
"software.amazon.smithy:smithy-validation-model:1.27.2",
]
}
}
},
}
},
...
});
Directory Structure
The TypeSafeApiProject
will create the following directory structure within its outdir
:
|_ model/
|_ src/
|_ main/
|_ smithy - your API definition if you chose ModelLanguage.SMITHY
|_ openapi - your API definition if you chose ModelLanguage.OPENAPI
|_ runtime/ - generated types, client, and server code in the languages you specified
|_ typescript
|_ python
|_ java
|_ infrastructure/ - generated infrastructure (you'll find only one directory in here based on your chosen infrastructure language)
|_ typescript
|_ python
|_ java
|_ documentation/ - generated documentation in the formats you specified
|_ html2
|_ html_redoc
|_ plantuml
|_ markdown
Smithy IDL
Please refer to the Smithy documentation for how to write models in Smithy. A basic example is provided below:
$version: "2"
namespace example.hello
use aws.protocols#restJson1
@title("A Sample Hello World API")
/// A sample smithy api
@restJson1
service Hello {
version: "1.0"
operations: [SayHello]
}
@readonly
@http(method: "GET", uri: "/hello")
operation SayHello {
input: SayHelloInput
output: SayHelloOutput
errors: [ApiError]
}
string Name
string Message
@input
structure SayHelloInput {
@httpQuery("name")
@required
name: Name
}
@output
structure SayHelloOutput {
@required
message: Message
}
@error("client")
structure ApiError {
@required
errorMessage: Message
}
Supported Protocols
Currently only AWS restJson1 is supported. Please ensure your service is annotated with the @restJson1
trait.
Multiple Files
You can split your definition into multiple files and folders, so long as they are all under the model/src/main/smithy
directory in your API project.
Authorizers
Smithy supports adding API Gateway authorizers in the model itself. Given that at model definition time one usually does not know the ARN of the user pool or lambda function for an authorizer, it is recommended to add the authorizer(s) in your Api CDK construct.
If using Smithy generated clients, some authorizer traits (eg sigv4) will include configuring the client for that particular method of authorization, so it can be beneficial to still define authorizers in the model. We therefore support specifying authorizers in both the model and the construct, but note that the construct will take precedence where the authorizer ID is the same.
OpenAPI Specification
Your model/src/main/openapi/main.yaml
file defines your api using OpenAPI Version 3.0.3. An example spec might look like:
openapi: 3.0.3
info:
version: 1.0.0
title: Example API
paths:
/hello:
get:
operationId: sayHello
parameters:
- in: query
name: name
schema:
type: string
required: true
responses:
'200':
description: Successful response
content:
'application/json':
schema:
$ref: '#/components/schemas/HelloResponse'
components:
schemas:
HelloResponse:
type: object
properties:
message:
type: string
required:
- message
You can divide your specification into multiple files using $ref
.
For example, you might choose to structure your spec as follows:
|_ model/src/main/openapi/
|_ main.yaml
|_ paths/
|_ index.yaml
|_ sayHello.yaml
|_ schemas/
|_ index.yaml
|_ helloResponse.yaml
Where main.yaml
looks as follows:
openapi: 3.0.3
info:
version: 1.0.0
title: Example API
paths:
$ref: './paths/index.yaml'
components:
schemas:
$ref: './schemas/index.yaml'
paths/index.yaml
:
/hello:
get:
$ref: './sayHello.yaml'
paths/sayHello.yaml
:
operationId: sayHello
parameters:
- in: query
name: name
schema:
type: string
required: true
responses:
'200':
description: Successful response
content:
'application/json':
schema:
$ref: '../schemas/helloResponse.yaml'
schemas/index.yaml
:
HelloResponse:
$ref: './helloResponse.yaml'
schemas/helloResponse.yaml
:
type: object
properties:
message:
type: string
required:
- message
Construct
A CDK construct is generated in the infrastructure/<language>
directory which provides a type-safe interface for creating an API Gateway API based on your model.
You can extend or instantiate this construct in your CDK infrastructure project. You'll get a type error if you forget to define an integration for an operation defined in your api.
import { Authorizers, Integrations } from '@aws-prototyping-sdk/type-safe-api';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { Api } from 'myapi-typescript-infra';
/**
* An example of how to wire lambda handler functions to the API
*/
export class SampleApi extends Api {
constructor(scope: Construct, id: string) {
super(scope, id, {
defaultAuthorizer: Authorizers.iam(),
integrations: {
// Every operation defined in your API must have an integration defined!
sayHello: {
integration: Integrations.lambda(new NodejsFunction(scope, 'say-hello')),
},
},
});
}
}
Sharing Integrations
If you would like to use the same integration for every operation (for example you'd like to use a single lambda function to service all requests with the in-built handler router), you can use the Operations.all
method from a generated runtime project to save repeating yourself:
import { Operations } from 'myapi-typescript-runtime';
import { Authorizers, Integrations } from '@aws-prototyping-sdk/type-safe-api';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { Api } from 'myapi-typescript-infra';
export class SampleApi extends Api {
constructor(scope: Construct, id: string) {
super(scope, id, {
defaultAuthorizer: Authorizers.iam(),
// Use the same integration for every operation.
integrations: Operations.all({
integration: Integrations.lambda(new NodejsFunction(scope, 'router')),
}),
});
}
}
TypeScript is demonstrated above, but this is also available in Java and Python.
Authorizers
The Api
construct allows you to define one or more authorizers for securing your API. An integration will use the defaultAuthorizer
unless an authorizer
is specified at the integration level. The following authorizers are supported:
Authorizers.none
- No authAuthorizers.iam
- AWS IAM (Signature Version 4)Authorizers.cognito
- Cognito user poolAuthorizers.custom
- A custom authorizer
Cognito Authorizer
To use the Cognito authorizer, one or more user pools must be provided. You can optionally specify the scopes to check if using an access token. You can use the withScopes
method to use the same authorizer but verify different scopes for individual integrations, for example:
export class SampleApi extends Api {
constructor(scope: Construct, id: string) {
const cognitoAuthorizer = Authorizers.cognito({
authorizerId: 'myCognitoAuthorizer',
userPools: [new UserPool(scope, 'UserPool')],
});
super(scope, id, {
defaultAuthorizer: cognitoAuthorizer,
integrations: {
// Everyone in the user pool can call this operation:
sayHello: {
integration: Integrations.lambda(new NodejsFunction(scope, 'say-hello')),
},
// Only users with the given scopes can call this operation
myRestrictedOperation: {
integration: Integrations.lambda(new NodejsFunction(scope, 'my-restricted-operation')),
authorizer: cognitoAuthorizer.withScopes('my-resource-server/my-scope'),
},
},
});
}
}
For more information about scopes or identity and access tokens, please see the API Gateway documentation.
Custom Authorizer
Custom authorizers use lambda functions to handle authorizing requests. These can either be simple token-based authorizers, or more complex request-based authorizers. See the API Gateway documentation for more details.
An example token-based authorizer (default):
Authorizers.custom({
authorizerId: 'myTokenAuthorizer',
function: new NodejsFunction(scope, 'authorizer'),
});
An example request-based handler. By default the identitySource will be method.request.header.Authorization
, however you can customise this as per the API Gateway documentation.
Authorizers.custom({
authorizerId: 'myRequestAuthorizer',
type: CustomAuthorizerType.REQUEST,
identitySource: 'method.request.header.MyCustomHeader, method.request.querystring.myQueryString',
function: new NodejsFunction(scope, 'authorizer'),
});
Integrations
Integrations are used by API Gateway to service requests.
Lambda Integration
Currently, the only built-in integration is a lambda integration. You can construct one using Integrations.lambda(yourLambdaFunction)
.
Custom Integrations
You can implement your own integrations by inheriting the Integration
class and implementing its render
method. This method is responsible for returning a snippet of OpenAPI which will be added as the x-amazon-apigateway-integration
for an operation. Please refer to the API Gateway Swagger Extensions documentation for more details.
You can also optionally override the grant
method if you need to use CDK to grant API Gateway access to invoke your integration.
Runtime: Clients
The generated runtime projects include clients which can be used for type-safe interaction with your API.
Typescript
The typescript-fetch OpenAPI generator is used to generate typescript client. This requires an implementation of fetch
to be passed to the client. In the browser one can pass the built in fetch, or in NodeJS you can use an implementation such as node-fetch.
Example usage of the client in a website:
import { Configuration, DefaultApi } from "myapi-typescript-runtime";
const client = new DefaultApi(new Configuration({
basePath: "https://xxxxxxxxxx.execute-api.ap-southeast-2.amazonaws.com",
fetchApi: window.fetch.bind(window),
}));
await client.sayHello({ name: "Jack" });
Python
The python OpenAPI generator is used to generate clients for python.
Example usage:
from myapi_python_runtime import ApiClient, Configuration
from myapi_python_runtime.api.default_api import DefaultApi
configuration = Configuration(
host = "https://xxxxxxxxxx.execute-api.ap-southeast-2.amazonaws.com"
)
with ApiClient(configuration) as api_client:
client = DefaultApi(api_client)
client.say_hello(
query_params={
'name': "name_example",
},
)
You'll find details about how to use the python client in the README.md in your generated runtime project.
Java
The java OpenAPI generator is used to generate clients for Java.
Example usage:
import com.generated.api.myapijavaruntime.runtime.api.DefaultApi;
import com.generated.api.myapijavaruntime.runtime.ApiClient;
import com.generated.api.myapijavaruntime.runtime.Configuration;
import com.generated.api.myapijavaruntime.runtime.models.HelloResponse;
ApiClient client = Configuration.getDefaultApiClient();
client.setBasePath("https://xxxxxxxxxx.execute-api.ap-southeast-2.amazonaws.com");
DefaultApi api = new DefaultApi(client);
HelloResponse response = api.sayHello("Adrian").execute()
You'll find more details about how to use the Java client in the README.md in your generated runtime project.
Lambda Handler Wrappers
Lambda handler wrappers are also importable from the generated runtime projects. These provide input/output type safety, ensuring that your API handlers return outputs that correspond to your model.
Typescript
import { sayHelloHandler } from "myapi-typescript-runtime";
export const handler = sayHelloHandler(async ({ input }) => {
return {
statusCode: 200,
body: {
message: `Hello ${input.requestParameters.name}!`,
},
};
});
Handler Router
The lambda handler wrappers can be used in isolation as handler methods for separate lambdas. If you would like to use a single lambda function to serve all requests, you can do so with the handlerRouter
.
import { handlerRouter, sayHelloHandler, sayGoodbyeHandler } from "myapi-typescript-runtime";
import { corsInterceptor } from "./interceptors";
import { sayGoodbye } from "./handlers/say-goodbye";
const sayHello = sayHelloHandler(async ({ input }) => {
return {
statusCode: 200,
body: {
message: `Hello ${input.requestParameters.name}!`,
},
};
});
export const handler = handlerRouter({
// Interceptors declared in this list will apply to all operations
interceptors: [corsInterceptor],
// Assign handlers to each operation here
handlers: {
sayHello,
sayGoodbye,
},
});
Python
from myapi_python_runtime.apis.tags.default_api_operation_config import say_hello_handler, SayHelloRequest, ApiResponse, SayHelloOperationResponses
from myapi_python_runtime.model.api_error import ApiError
from myapi_python_runtime.model.hello_response import HelloResponse
@say_hello_handler
def handler(input: SayHelloRequest, **kwargs) -> SayHelloOperationResponses:
return ApiResponse(
status_code=200,
body=HelloResponse(message="Hello {}!".format(input.request_parameters["name"])),
headers={}
)
Handler Router
The lambda handler wrappers can be used in isolation as handler methods for separate lambdas. If you would like to use a single lambda function to serve all requests, you can do so with the handler_router
.
from myapi_python_runtime.apis.tags.default_api_operation_config import say_hello_handler, SayHelloRequest, ApiResponse, SayHelloOperationResponses, handler_router, HandlerRouterHandlers
from myapi_python_runtime.model.api_error import ApiError
from myapi_python_runtime.model.hello_response import HelloResponse
from other_handlers import say_goodbye
from my_interceptors import cors_interceptor
@say_hello_handler
def say_hello(input: SayHelloRequest, **kwargs) -> SayHelloOperationResponses:
return ApiResponse(
status_code=200,
body=HelloResponse(message="Hello {}!".format(input.request_parameters["name"])),
headers={}
)
handler = handler_router(
# Interceptors defined here will apply to all operations
interceptors=[cors_interceptor],
handlers=HandlerRouterHandlers(
say_hello=say_hello,
say_goodbye=say_goodbye
)
)
Java
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHello;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHello200Response;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHelloRequestInput;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHelloResponse;
import com.generated.api.myapijavaruntime.runtime.model.HelloResponse;
public class SayHelloHandler extends SayHello {
@Override
public SayHelloResponse handle(SayHelloRequestInput sayHelloRequestInput) {
return SayHello200Response.of(HelloResponse.builder()
.message(String.format("Hello %s", sayHelloRequestInput.getInput().getRequestParameters().getName()))
.build());
}
}
Handler Router
The lambda handler wrappers can be used in isolation as handler methods for separate lambdas. If you would like to use a single lambda function to serve all requests, you can do so by extending the HandlerRouter
class.
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayGoodbye;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.HandlerRouter;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.Interceptors;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHello;
import java.util.Arrays;
import java.util.List;
// Interceptors defined here apply to all operations
@Interceptors({ TimingInterceptor.class })
public class ApiHandlerRouter extends HandlerRouter {
// You must implement a method to return a handler for every operation
@Override
public SayHello sayHello() {
return new SayHelloHandler();
}
@Override
public SayGoodbye sayGoodbye() {
return new SayGoodbyeHandler();
}
}
Interceptors
The lambda handler wrappers allow you to pass in a chain of handler functions to handle the request. This allows you to implement middleware / interceptors for handling requests. Each handler function may choose whether or not to continue the handler chain by invoking chain.next
.
Typescript
In typescript, interceptors are passed as separate arguments to the generated handler wrapper, in the order in which they should be executed. Call request.chain.next(request)
from an interceptor to delegate to the rest of the chain to handle a request. Note that the last handler in the chain (ie the actual request handler which transforms the input to the output) should not call chain.next
.
import {
sayHelloHandler,
ChainedRequestInput,
OperationResponse,
} from "myapi-typescript-runtime";
// Interceptor to wrap invocations in a try/catch, returning a 500 error for any unhandled exceptions.
const tryCatchInterceptor = async <RequestParameters, RequestArrayParameters, RequestBody, Response>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>
): Promise<Response | OperationResponse<500, { errorMessage: string }>> => {
try {
return await request.chain.next(request);
} catch (e: any) {
return { statusCode: 500, body: { errorMessage: e.message }};
}
};
// tryCatchInterceptor is passed first, so it runs first and calls the second argument function (the request handler) via chain.next
export const handler = sayHelloHandler(tryCatchInterceptor, async ({ input }) => {
return {
statusCode: 200,
body: {
message: `Hello ${input.requestParameters.name}!`,
},
};
});
Another example interceptor might be to record request time metrics. The example below includes the full generic type signature for an interceptor:
import {
ChainedRequestInput,
} from 'myapi-typescript-runtime';
const timingInterceptor = async <RequestParameters, RequestArrayParameters, RequestBody, Response>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>
): Promise<Response> => {
const start = Date.now();
const response = await request.chain.next(request);
const end = Date.now();
console.log(`Took ${end - start} ms`);
return response;
};
Interceptors may mutate the interceptorContext
to pass state to further interceptors or the final lambda handler, for example an identityInterceptor
might want to extract the authenticated user from the request so that it is available in handlers.
import {
LambdaRequestParameters,
LambdaHandlerChain,
} from 'myapi-typescript-runtime';
const identityInterceptor = async <RequestParameters, RequestArrayParameters, RequestBody, Response>(
request: ChainedRequestInput<RequestParameters, RequestArrayParameters, RequestBody, Response>
): Promise<Response> => {
const authenticatedUser = await getAuthenticatedUser(request.event);
return await request.chain.next({
...request,
interceptorContext: {
...request.interceptorContext,
authenticatedUser,
},
});
};
Python
In Python, a list of interceptors can be passed as a keyword argument to the generated lambda handler decorator, for example:
from myapi_python_runtime.apis.tags.default_api_operation_config import say_hello_handler, SayHelloRequest, ApiResponse, SayHelloOperationResponses
from myapi_python_runtime.model.api_error import ApiError
from myapi_python_runtime.model.hello_response import HelloResponse
@say_hello_handler(interceptors=[timing_interceptor, try_catch_interceptor])
def handler(input: SayHelloRequest, **kwargs) -> SayHelloOperationResponses:
return ApiResponse(
status_code=200,
body=HelloResponse(message="Hello {}!".format(input.request_parameters["name"])),
headers={}
)
Writing an interceptor is just like writing a lambda handler. Call chain.next(input)
from an interceptor to delegate to the rest of the chain to handle a request.
import time
from myapi_python_runtime.apis.tags.default_api_operation_config import ChainedApiRequest, ApiResponse
def timing_interceptor(input: ChainedApiRequest) -> ApiResponse:
start = int(round(time.time() * 1000))
response = input.chain.next(input)
end = int(round(time.time() * 1000))
print("Took {} ms".format(end - start))
return response
Interceptors may choose to return different responses, for example to return a 500 response for any unhandled exceptions:
import time
from myapi_python_runtime.model.api_error import ApiError
from myapi_python_runtime.apis.tags.default_api_operation_config import ChainedApiRequest, ApiResponse
def try_catch_interceptor(input: ChainedApiRequest) -> ApiResponse:
try:
return input.chain.next(input)
except Exception as e:
return ApiResponse(
status_code=500,
body=ApiError(errorMessage=str(e)),
headers={}
)
Interceptors are permitted to mutate the "interceptor context", which is a Dict[str, Any]
. Each interceptor in the chain, and the final handler, can access this context:
def identity_interceptor(input: ChainedApiRequest) -> ApiResponse:
input.interceptor_context["AuthenticatedUser"] = get_authenticated_user(input.event)
return input.chain.next(input)
Interceptors can also mutate the response returned by the handler chain. An example use case might be adding cross-origin resource sharing headers:
def add_cors_headers_interceptor(input: ChainedApiRequest) -> ApiResponse:
response = input.chain.next(input)
return ApiResponse(
status_code=response.status_code,
body=response.body,
headers={
**response.headers,
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "*"
}
)
Java
In Java, interceptors can be added to a handler via the @Interceptors
class annotation:
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.Interceptors;
@Interceptors({ TimingInterceptor.class, TryCatchInterceptor.class })
public class SayHelloHandler extends SayHello {
@Override
public SayHelloResponse handle(SayHelloRequestInput sayHelloRequestInput) {
return SayHello200Response.of(HelloResponse.builder()
.message(String.format("Hello %s", sayHelloRequestInput.getInput().getRequestParameters().getName()))
.build());
}
}
To write an interceptor, you can implement the Interceptor
interface. For example, a timing interceptor:
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.Interceptor;
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.ChainedRequestInput;
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.Response;
public class TimingInterceptor<Input> implements Interceptor<Input> {
@Override
public Response handle(ChainedRequestInput<Input> input) {
long start = System.currentTimeMillis();
Response res = input.getChain().next(input);
long end = System.currentTimeMillis();
System.out.printf("Took %d ms%n", end - start);
return res;
}
}
Interceptors may choose to return different responses, for example to return a 500 response for any unhandled exceptions:
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.Interceptor;
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.ChainedRequestInput;
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.Response;
import com.generated.api.myjavaapiruntime.runtime.api.Handlers.ApiResponse;
import com.generated.api.myjavaapiruntime.runtime.model.ApiError;
public class TryCatchInterceptor<Input> implements Interceptor<Input> {
@Override
public Response handle(ChainedRequestInput<Input> input) {
try {
return input.getChain().next(input);
} catch (Exception e) {
return ApiResponse.builder()
.statusCode(500)
.body(ApiError.builder()
.errorMessage(e.getMessage())
.build().toJson())
.build();
}
}
}
Interceptors are permitted to mutate the "interceptor context", which is a Map<String, Object>
. Each interceptor in the chain, and the final handler, can access this context:
public class IdentityInterceptor<Input> implements Interceptor<Input> {
@Override
public Response handle(ChainedRequestInput<Input> input) {
input.getInterceptorContext().put("AuthenticatedUser", this.getAuthenticatedUser(input.getEvent()));
return input.getChain().next(input);
}
}
Interceptors can also mutate the response returned by the handler chain. An example use case might be adding cross-origin resource sharing headers:
public static class AddCorsHeadersInterceptor<Input> implements Interceptor<Input> {
@Override
public Response handle(ChainedRequestInput<Input> input) {
Response res = input.getChain().next(input);
res.getHeaders().put("Access-Control-Allow-Origin", "*");
res.getHeaders().put("Access-Control-Allow-Headers", "*");
return res;
}
}
Interceptors with Dependency Injection
Interceptors referenced by the @Interceptors
annotation must be constructable with no arguments. If more complex instantiation of your interceptor is required (for example if you are using dependency injection or wish to pass configuration to your interceptor), you may instead override the getInterceptors
method in your handler:
public class SayHelloHandler extends SayHello {
@Override
public List<Interceptor<SayHelloInput>> getInterceptors() {
return Arrays.asList(
new MyConfiguredInterceptor<>(42),
new MyOtherConfiguredInterceptor<>("configuration"));
}
@Override
public SayHelloResponse handle(SayHelloRequestInput sayHelloRequestInput) {
return SayHello200Response.of(HelloResponse.builder()
.message(String.format("Hello %s!", sayHelloRequestInput.getInput().getRequestParameters().getName()))
.build());
}
}
Quick Start: Python
This guide assumes you want to write your CDK infrastructure in Python and your lambda handlers in Python, however note that you your infrastructure language and lambda handler language(s) are not tied to one another, you can mix and match as you like. Just specify the language in runtime.languages
for any language you would like to write lambda handlers in.
Create Your API Project
Use the project in your .projenrc.ts
. It can either be part of an nx-monorepo
(recommended) or used in a standalone fashion.
import { NxMonorepoProject } from "@aws-prototyping-sdk/nx-monorepo";
import { TypeSafeApiProject } from "@aws-prototyping-sdk/type-safe-api";
import { AwsCdkTypeScriptApp } from "projen/lib/awscdk";
import { PythonProject } from "projen/lib/python";
// Create the monorepo
const monorepo = new NxMonorepoProject({
name: 'monorepo',
defaultReleaseBranch: "main",
});
// Create the API project
const api = new TypeSafeApiProject({
name: "myapi",
parent: monorepo,
outdir: 'packages/api',
// Smithy as the model language. You can also use ModelLanguage.OPENAPI
model: {
language: ModelLanguage.SMITHY,
options: {
smithy: {
serviceName: {
namespace: 'com.mycompany',
serviceName: 'MyApi',
},
},
},
},
// Generate client and server types in TypeScript, Python, and Java
runtime: {
languages: [Language.TYPESCRIPT, Language.PYTHON, Language.JAVA],
},
// Generate CDK infrastructure in Python
infrastructure: {
language: Language.PYTHON,
},
// Generate HTML documentation
documentation: {
formats: [DocumentationFormat.HTML_REDOC]
},
});
// Create a project for our lambda handlers written in python
const lambdas = new PythonProject({
name: "lambdas",
parent: monorepo,
outdir: 'packages/lambdas',
authorEmail: "me@example.com",
authorName: "me",
moduleName: "lambdas",
version: "1.0.0",
// Poetry is used to simplify local python dependencies
poetry: true,
});
// Add a local dependency on the generated python runtime
lambdas.addDependency(`${api.runtime.python!.name}@{path="${path.relative(lambdas.outdir, api.runtime.python!.outdir)}", develop=true}`);
monorepo.addImplicitDependency(lambdas, api.runtime.python!);
// Add commands to the lambda project's package task to create a distributable which can be deployed to AWS Lambda
lambdas.packageTask.exec(`mkdir -p lambda-dist && rm -rf lambda-dist/*`);
lambdas.packageTask.exec(`cp -r ${lambdas.moduleName} lambda-dist/${lambdas.moduleName}`);
lambdas.packageTask.exec(`poetry export --without-hashes --format=requirements.txt > lambda-dist/requirements.txt`);
lambdas.packageTask.exec(`pip install -r lambda-dist/requirements.txt --target lambda-dist --upgrade`);
lambdas.gitignore.addPatterns('lambda-dist');
// Create a CDK infrastructure project
const infra = new AwsCdkPythonApp({
name: "infra",
parent: monorepo,
outdir: 'packages/infra',
authorEmail: "me@example.com",
authorName: "me",
cdkVersion: "2.0.0",
moduleName: "infra",
version: "1.0.0",
poetry: true,
});
// The infrastructure project depends on the python types, python infrastructure, and the lambda package
infra.addDependency(`${api.runtime.python!.name}@{path="${path.relative(infra.outdir, api.runtime.python!.outdir)}", develop=true}`);
monorepo.addImplicitDependency(infra, api.runtime.python!);
infra.addDependency(`${api.infrastructure.python!.name}@{path="${path.relative(infra.outdir, api.infrastructure.python!.outdir)}", develop=true}`);
monorepo.addImplicitDependency(infra, api.infrastructure.python!);
infra.addDependency(`${lambdas.name}@{path="${path.relative(infra.outdir, lambdas.outdir)}", develop=true}`);
monorepo.addImplicitDependency(infra, lambdas);
monorepo.synth();
Use the CDK Construct
In your CDK application, consume the Api
construct, vended from the generated Python infrastructure package.
import os
from aws_cdk import Stack
from constructs import Construct
from aws_cdk.aws_lambda import LayerVersion, Code, Function, Runtime
from aws_prototyping_sdk.type_safe_api import Authorizers, TypeSafeApiIntegration, Integrations
from myapi_python_runtime.apis.tags.default_api_operation_config import OperationConfig
from myapi_python_infra.api import Api
from pathlib import Path
from os import path
class MyStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
super().__init__(scope, construct_id, **kwargs)
# Use the generated Api construct
self.api = Api(self, 'Api',
default_authorizer=Authorizers.iam(),
integrations=OperationConfig(
say_hello=TypeSafeApiIntegration(
# Create a python lambda function from our "lambda-dist" package
integration=Integrations.lambda_(Function(self, 'SayHello',
runtime=Runtime.PYTHON_3_9,
code=Code.from_asset(path.join("..", "lambdas", "lambda-dist")),
handler="lambdas.say_hello.handler",
)),
),
),
)
Implement a Lambda Handler
In your lambdas
project you can define your lambda handler in its source directory, eg lambdas/lambdas/say_hello.py
:
from myapi_python_runtime.model.say_hello_response_content import SayHelloResponseContent
from myapi_python_runtime.apis.tags.default_api_operation_config import say_hello_handler,
SayHelloRequest, SayHelloOperationResponses, ApiResponse
@say_hello_handler
def handler(input: SayHelloRequest, **kwargs) -> SayHelloOperationResponses:
return ApiResponse(
status_code=200,
body=SayHelloResponseContent(message="Hello {}".format(input.request_parameters["name"])),
headers={}
)
Quick Start: Java
This guide assumes you want to write your CDK infrastructure in Java and your lambda handlers in Java, however note that you your infrastructure language and lambda handler language(s) are not tied to one another, you can mix and match as you like.
Create Your API Project
Use the project in your .projenrc.ts
. It can either be part of an nx-monorepo
(recommended) or used in a standalone fashion.
import { NxMonorepoProject } from "@aws-prototyping-sdk/nx-monorepo";
import { TypeSafeApiProject } from "@aws-prototyping-sdk/type-safe-api";
import { AwsCdkTypeScriptApp } from "projen/lib/awscdk";
import { JavaProject } from "projen/lib/java";
// Create the monorepo
const monorepo = new NxMonorepoProject({
name: 'monorepo',
defaultReleaseBranch: "main",
});
// Create the API project
const api = new TypeSafeApiProject({
name: "myapi",
parent: monorepo,
outdir: 'packages/api',
// Smithy as the model language. You can also use ModelLanguage.OPENAPI
model: {
language: ModelLanguage.SMITHY,
options: {
smithy: {
serviceName: {
namespace: 'com.mycompany',
serviceName: 'MyApi',
},
},
},
},
// Generate client and server types in TypeScript, Python and Java
runtime: {
languages: [Language.TYPESCRIPT, Language.PYTHON, Language.JAVA],
},
// Generate CDK infrastructure in Java
infrastructure: {
language: Language.JAVA,
},
// Generate HTML documentation
documentation: {
formats: [DocumentationFormat.HTML_REDOC],
},
});
const lambdas = new JavaProject({
name: "lambdas",
parent: monorepo,
outdir: 'packages/lambdas',
artifactId: "lambdas",
groupId: "com.my.api",
version: "1.0.0",
});
// The lambdas package needs a dependency on the generated java runtime
lambdas.addDependency(`${api.runtime.java!.pom.groupId}/${api.runtime.java!.pom.artifactId}@${api.runtime.java!.pom.version}`);
lambdas.pom.addRepository({
url: `file://${path.relative(lambdas.outdir, api.runtime.java!.outdir)}/dist/java`,
id: 'java-api-runtime',
});
monorepo.addImplicitDependency(lambdas, api.runtime.java!);
// Use the maven shade plugin to build a "super jar" which we can deploy to AWS Lambda
lambdas.pom.addPlugin("org.apache.maven.plugins/maven-shade-plugin@3.3.0", {
configuration: {
createDependencyReducedPom: false,
},
executions: [
{
id: "shade-task",
phase: "package",
goals: ["shade"],
},
],
});
const infra = new AwsCdkJavaApp({
name: "infra",
parent: monorepo,
outdir: 'packages/infra',
artifactId: "infra",
groupId: "com.my.api",
mainClass: "com.my.api.MyApp",
version: "1.0.0",
cdkVersion: "2.0.0",
});
// Add a dependency on the generated CDK infrastructure
infra.addDependency(`${api.infrastructure.java!.pom.groupId}/${api.infrastructure.java!.pom.artifactId}@${api.infrastructure.java!.pom.version}`);
infra.pom.addRepository({
url: `file://${path.relative(infra.outdir, api.infrastructure.java!.outdir)}/dist/java`,
id: 'java-api-infra',
});
monorepo.addImplicitDependency(infra, api.infrastructure.java!);
// Make sure the java lambda builds before our CDK infra
monorepo.addImplicitDependency(infra, lambdas);
monorepo.synth();
Use the CDK Construct
In your CDK application, consume the Api
construct, vended from the generated Java infrastructure package.
package com.my.api;
import com.generated.api.myapijavainfra.infra.Api;
import com.generated.api.myapijavainfra.infra.ApiProps;
import com.generated.api.myapijavaruntime.runtime.api.OperationConfig;
import software.amazon.awscdk.Duration;
import software.amazon.awscdk.services.apigateway.CorsOptions;
import software.amazon.awscdk.services.lambda.Code;
import software.amazon.awscdk.services.lambda.Function;
import software.amazon.awscdk.services.lambda.FunctionProps;
import software.amazon.awscdk.services.lambda.Runtime;
import software.aws.awsprototypingsdk.typesafeapi.Authorizers;
import software.aws.awsprototypingsdk.typesafeapi.Integrations;
import software.aws.awsprototypingsdk.typesafeapi.TypeSafeApiIntegration;
import software.amazon.awscdk.App;
import software.amazon.awscdk.Stack;
import java.util.Arrays;
public class MyApp {
public static void main(final String[] args) {
App app = new App();
Stack s = new Stack(app, "infra");
// Declare the API construct to deploy the API Gateway resources
new Api(s, "Api", ApiProps.builder()
.defaultAuthorizer(Authorizers.iam())
.corsOptions(CorsOptions.builder()
.allowOrigins(Arrays.asList("*"))
.allowMethods(Arrays.asList("*"))
.build())
.integrations(OperationConfig.<TypeSafeApiIntegration>builder()
.sayHello(TypeSafeApiIntegration.builder()
.integration(Integrations.lambda(
// Point the lambda function to our built jar from the "lambdas" package
new Function(s, "say-hello", FunctionProps.builder()
.code(Code.fromAsset("../lambdas/dist/java/com/my/api/lambdas/1.0.0/lambdas-1.0.0.jar"))
.handler("com.my.api.SayHelloHandler")
.runtime(Runtime.JAVA_11)
.timeout(Duration.seconds(30))
.build())))
.build())
.build())
.build());
app.synth();
}
}
Implement a Lambda Handler
In your lambdas
project you can define your lambda handler in its source directory, eg lambdas/src/main/java/com/my/api/SayHelloHandler.java
:
package com.my.api;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHello;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHello200Response;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHelloRequestInput;
import com.generated.api.myapijavaruntime.runtime.api.Handlers.SayHelloResponse;
import com.generated.api.myapijavaruntime.runtime.model.SayHelloResponseContent;
/**
* An example lambda handler which uses the generated handler wrapper class (Handlers.SayHello) to manage marshalling
* inputs and outputs.
*/
public class SayHelloHandler extends SayHello {
@Override
public SayHelloResponse handle(SayHelloRequestInput sayHelloRequestInput) {
return SayHello200Response.of(SayHelloResponseContent.builder()
.message(String.format("Hello %s", sayHelloRequestInput.getInput().getRequestParameters().getName()))
.build());
}
}
Other Details
Customising Generated Types/Infrastructure Projects
By default, the generated types and infrastructure projects are configured automatically, including their project names. You can customise the generated projects using the runtime.options.<language>
or infrastructure.options.<language>
properties when constructing the TypeSafeApiProject
.
AWS WAFv2 Web ACL
By default, a Web ACL is deployed and attached to your API Gateway Rest API with the "AWSManagedRulesCommonRuleSet", which provides protection against exploitation of a wide range of vulnerabilities, including some of the high risk and commonly occurring vulnerabilities described in OWASP publications such as OWASP Top 10.
You can customise the Web ACL configuration via the webAclOptions
of your Api
CDK construct, eg:
export class SampleApi extends Api {
constructor(scope: Construct, id: string) {
super(scope, id, {
integrations: { ... },
webAclOptions: {
// Allow access only to specific CIDR ranges
cidrAllowList: {
cidrType: 'IPV4',
cidrRanges: ['1.2.3.4/5'],
},
// Pick from the set here: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html
managedRules: [
{ vendor: 'AWS', name: 'AWSManagedRulesSQLiRuleSet' },
],
},
});
}
}
You can remove the Web ACL entirely with webAclOptions: { disable: true }
- you may wish to use this if you'd like to set up a Web ACL yourself with more control over the rules.
Smithy IntelliJ Plugin
The Smithy-based projects are compatible with the Smithy IntelliJ Plugin, which provides syntax highlighting and auto-complete for your Smithy model. To make use of it, perform the following steps:
- Install the "Smithy" plugin (under
Preferences -> Plugins
) - Right-click on the
smithy/build.gradle
file in your Smithy API project - Select "Link Gradle Project"
Tagging Operations
Operations can be grouped together into logical collections via tags. This can be achieved in Smithy with the @tags
trait:
@tags(["pets", "users"])
operation PurchasePet {
...
}
Or in OpenAPI using the tags
property:
paths:
/pets/purchase:
post:
operationId: purchasePet
tags:
- pets
- users
...
When multiple tags are used, the "first" tag is considered to be the API that the operation belongs to, so in the generated client, the above example operation would be included in the PetsApi
client but not the UsersApi
client.
Multiple tags are still useful for documentation generation, for example DocumentationFormat.HTML_REDOC
will group operations by tag in the side navigation bar.
If you would like to introduce tags without breaking existing clients, we recommend first adding a tag named default
to all operations.
⚠️ Important Note: Smithy versions below 1.28.0
sort tags in alphabetical order and so the "first" tag will be the earliest in the alphabet. Therefore, if using tags with older versions of Smithy, we recommend prefixing your desired first tag with an underscore (for example _default
). This is rectified in 1.28.0
, where tag order from the @tags
trait is preserved.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Hashes for aws_prototyping_sdk.type_safe_api-0.15.5.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | 10974b90cd530fd415f7957fc1b94662b71c034b75d162a87798dcd3f28aa8aa |
|
MD5 | 88b9817c025a129c595237b8c47c289f |
|
BLAKE2b-256 | 6194187bf70f97568c29bc042f7853c05501e141de5c4a0255023229fb3979cc |
Hashes for aws_prototyping_sdk.type_safe_api-0.15.5-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 759394657ffe251a44c19de4f8ae2a064b4fc2290797a4a1afef12a9e973e0c3 |
|
MD5 | 735fc6d2025511cccf54eb776b887d9a |
|
BLAKE2b-256 | 27206b382a2d628e717b9c7f294ed4c7008279ea054c2523231943ae24087cdc |