@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],
},
// Generate react-query hooks to interact with the UI from a React website
library: {
libraries: [Library.TYPESCRIPT_REACT_QUERY_HOOKS],
},
});
// Create a CDK infrastructure project. Can also consider PDKPipelineTsProject as an alternative!
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 (ie within the infra
project we created), 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 the above CDK application, we used NodejsFunction
with the entry point as say-hello.ts
, so we can define the lambda function in the same infra
project.
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
|_ library/ - generated libraries if specified
|_ typescript-react-query-hooks
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
For integrating an API operation with a lambda, you can use Integrations.lambda(yourLambdaFunction)
.
Mock Integration
To mock an API operation, you can use Integrations.mock
. API gateway will respond with the status code and body provided, eg:
Integrations.mock({ statusCode: 200, body: JSON.stringify({ message: "hello world!" }) })
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());
}
}
Libraries
Libraries are generated code projects which are not fully-fledged runtime languages.
TypeScript React Query Hooks
This library contains generated react-query hooks for interacting with your API from a React website. You can generate these by adding the following options to your TypeSafeApiProject
in your .projenrc
:
new TypeSafeApiProject({
library: {
libraries: [Library.TYPESCRIPT_REACT_QUERY_HOOKS],
},
...
});
Usage in a React Website
First, make sure you add a dependency on the generated hooks library, eg in your .projenrc
:
const api = new TypeSafeApiProject({ ... });
new CloudscapeReactTsWebsite({
...,
deps: [
...
api.library.typescriptReactQueryHooks!.package.packageName,
],
});
Next, create an instance of the API client (making sure to set the base URL and fetch instance). For example:
// NB: client may be named differently if you have tagged your operations
import { DefaultApi } from "myapi-typescript-react-query-hooks";
export const useApiClient = () =>
useMemo(
() =>
new DefaultApi(
new Configuration({
basePath:
"https://example123.execute-api.ap-southeast-2.amazonaws.com/prod",
fetchApi: window.fetch.bind(window),
})
),
[]
);
Note that if you are using the Cloudscape React Website with AWS NorthStar and IAM (Sigv4) Auth for your API, you can use NorthStar's useSigv4Client()
hook to create
an instance of fetch
which will sign requests with the logged in user's credentials. For example:
export const useApiClient = () => {
const client = useSigv4Client();
return useMemo(
() =>
new DefaultApi(
new Configuration({
basePath:
"https://example123.execute-api.ap-southeast-2.amazonaws.com/prod",
fetchApi: client,
})
),
[client]
);
};
Next, instantiate the client provider above where you would like to use the hooks in your component hierarchy (such as above your router). For example:
// NB: client provider may be named differently if you have tagged your operations
import { DefaultApiClientProvider } from "myapi-typescript-react-query-hooks";
const api = useApiClient();
return (
<DefaultApiClientProvider apiClient={api}>
{/* Components within the provider may make use of the hooks */}
</DefaultApiClientProvider>
);
Finally, you can import and use your generated hooks. For example:
import { useSayHello } from "myapi-typescript-react-query-hooks";
export const MyComponent: FC<MyComponentProps> = () => {
const sayHello = useSayHello({ name: "World" });
return sayHello.isLoading ? (
<p>Loading...</p>
) : sayHello.isError ? (
<p>Error!</p>
) : (
<h1>{sayHello.data.message}</h1>
);
};
Paginated Operations
You can generate useInfiniteQuery
hooks instead of useQuery
hooks for paginated API operations, by making use of the vendor extension x-paginated
in your operation in the OpenAPI specification. You must specify both the inputToken
and outputToken
, which indicate the properties from the input and output used for pagination. For example in OpenAPI:
paths:
/pets:
get:
x-paginated:
# Input property with the token to request the next page
inputToken: nextToken
# Output property with the token to request the next page
outputToken: nextToken
parameters:
- in: query
name: nextToken
schema:
type: string
required: true
responses:
200:
description: Successful response
content:
application/json:
schema:
type: object
properties:
nextToken:
type: string
In Smithy, until custom vendor extensions can be rendered via traits, you can add the x-paginated
vendor extension via smithyBuildOptions
in your TypeSafeApiProject
, for example:
new TypeSafeApiProject({
model: {
language: ModelLanguage.SMITHY,
options: {
smithy: {
serviceName: {
namespace: 'com.mycompany',
serviceName: 'MyApi',
},
smithyBuildOptions: {
projections: {
openapi: {
plugins: {
openapi: {
jsonAdd: {
// Add the x-paginated vendor extension to the GET /pets operation
'/paths/~1pets/get/x-paginated': {
inputToken: 'nextToken',
outputToken: 'nextToken',
},
},
},
},
},
},
},
},
},
},
...
});
Custom QueryClient
If you wish to customise the react-query QueryClient
, pass a custom instance to the client provider, eg:
import { DefaultApiClientProvider } from "myapi-typescript-react-query-hooks";
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({ ... });
return (
<DefaultApiClientProvider apiClient={api} client={queryClient}>
{/* Components within the provider may make use of the hooks */}
</DefaultApiClientProvider>
);
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
monorepo.addPythonPoetryDependency(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
monorepo.addPythonPoetryDependency(infra, api.runtime.python!);
monorepo.addPythonPoetryDependency(infra, api.infrastructure.python!);
monorepo.addPythonPoetryDependency(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
monorepo.addJavaDependency(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
monorepo.addJavaDependency(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.
Smithy Model Libraries and Dependencies
You can instantiate the TypeSafeApiModelProject on its own to create a standalone Smithy model library.
You can consume the library using the addSmithyDeps
method, which adds a local file dependency on the built Smithy jar.
// Standalone model project, used as our model library
const shapes = new TypeSafeApiModelProject({
name: "shapes",
parent: monorepo,
outdir: "packages/shapes",
modelLanguage: ModelLanguage.SMITHY,
modelOptions: {
smithy: {
serviceName: {
namespace: "com.my.shared.shapes",
serviceName: "Ignored",
},
},
},
});
const api = new TypeSafeApiProject({ ... });
// Add the implicit monorepo dependency (if using the nx-monorepo) to ensure the shape library is built before the api model
monorepo.addImplicitDependency(api.model, shapes);
// Add a local file dependency on the built shapes jar
api.model.smithy!.addSmithyDeps(shapes.smithy!);
Local API Development Server
You can use the AWS SAM CLI to run a local development server for your API. You can achieve this using the following steps:
- Synthesize your CDK stack containing your
Api
construct (this might be yourAwsCdkTypeScriptApp
project for example), with the context propertytype-safe-api-local
set totrue
, for example:
cd packages/infra
npx cdk synth --context type-safe-api-local=true
- Use the AWS SAM CLI to start the local development server, pointing it at the cloudformation template synthesized from the above command (note that the command will fail if docker is not running)
sam local start-api -t cdk.out/<your-stack>.template.json
You will need to repeat the above steps every time you make a code change for them to be reflected in your local development server.
See the AWS SAM CLI Reference for more information on the commands to run.
Make sure you do not deploy your CDK stack with type-safe-api-local
set to true
, since this uses an inline API definition which bloats the CloudFormation template and can exceed the maximum template size depending on the size of your API.
Limitations
Note that there is currently a limitation with SAM CLI where it does not support mock integrations, which means that the development server will not respond to OPTIONS requests even if you specified corsOptions: { ... }
in your Api
construct. This is being tracked as a feature request here.
Note also that your API business logic may include operations which do not work locally, or may interact with real AWS resources depending on the AWS credentials you start your local development server with.
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.18.13.tar.gz
Algorithm | Hash digest | |
---|---|---|
SHA256 | e76fe74a533424edc4dda5b40e795690656f23475ef4b29161a77c9df9f2ae61 |
|
MD5 | b0e8eb4d8e3e76ce5c106f9fedcb097a |
|
BLAKE2b-256 | 99039d3cb0a9bf8c9787bf57a977c24ae2f7f62d9a7e589427197d55f9c1f1c5 |
Hashes for aws_prototyping_sdk.type_safe_api-0.18.13-py3-none-any.whl
Algorithm | Hash digest | |
---|---|---|
SHA256 | 922fe289b54b34e3f3752085d9db574108db60ad2d87f377d942642a0528c4e3 |
|
MD5 | 1df4710c024e6d93fbdfac8a63bc17cf |
|
BLAKE2b-256 | 3b77946a3b6a07a54c55ad5cc80ee78a69a8838296f365520ee3a46c8439ebb9 |