When developing an API (Application Programming Interface) from scratch, there are many things to think about. Where to host it, how to deal with load balancing, which API framework to use… the list goes on. In short, developing an API from scratch takes a lot of time and effort. But AWS allows you to create an API in minutes. In this post, I’ll show you how.
Prerequisites
Before you start, you’ll need the following things:
- An AWS account
- An Administrator IAM User with which to use the AWS CLI
- AWS CLI set up on your local machine
Note that AWS will charge you for creating and hosting this API if you don’t have free tier on your account.
Step 1: Create a CloudFormation Template
CloudFormation templates are configuration documents that can be used to tell AWS what resources and services to provision. They are useful because you can provision a number of different resources at once. Your provisioned resources are called a ‘stack’. When you want to deprovision those same resources, you only need to issue one command to delete all the resources in the ‘stack’ at once.
CloudFormation templates can be in yaml or json format. For this example, the template will be in yaml format.
At the top of the CloudFormation template (I’m going to call it CFT from now onwards), place the version and description lines:
AWSTemplateFormatVersion: "2010-09-09" Description: Creates a simple API endpoint that returns a list of links
The AWSTemplateFormatVersion
field tells AWS how to interpret the CFT. Currently there is only one version available. The Description
field is where we add the description of what this CFT is going to build.
Next we’ll add a Resources
section, which is contains the bulk of the CFT. The first resource we’ll add is a Lambda execution IAM role with associated policies, requestHandlerLambdaExecutionRole
:
Resources: requestHandlerLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: "root" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:PutLogEvents - logs:CreateLogStream Resource: "*" RoleName: requestHandlerLambdaExecutionRole
This role allows the Lambda function (which comes later in the CFT) to run. The Lambda function is a piece of code that runs on demand. In our case, this Lambda will run whenever someone makes a request to the API we are building. The AssumeRolePolicyDocument
section gives the Lambda service permission to assume this role. The PolicyDocument
section determines what the Lambda function is allowed to do. In this case, it is given permission to generate logs.
The next resource in the CFT is the Lambda function itself, the requestHandler
resource:
requestHandler: Type: "AWS::Lambda::Function" Properties: Code: ZipFile: !Sub | import json def lambda_handler(event, context): try: print(event) print(context) # Because this Lambda function is called by an API Gateway proxy integration # the result object must use the following structure. return { "statusCode": 200, "isBase64Encoded": False, "body": json.dumps({ "links": ["https://www.google.com", "https://www.amazon.com"], }), "headers": { 'Access-Control-Allow-Origin': '*', }, } except Exception as err: print(str(err)) # If there is an error during processing, catch it and return # from the Lambda function successfully. Specify a 500 HTTP status # code and provide an error message in the body. This will provide a # more meaningful error response to the end client. error_response(str(err), context.aws_request_id) def error_response(error_message, aws_request_id): return { "statusCode": 500, "isBase64Encoded": False, "body": json.dumps({ "Error": error_message, "Reference": aws_request_id, }), "headers": { 'Access-Control-Allow-Origin': '*', }, } Description: Lambda function for processing api endpoint requests FunctionName: requestHandler Handler: index.lambda_handler Role: Fn::GetAtt: - "requestHandlerLambdaExecutionRole" - "Arn" Runtime: python3.8 DependsOn: - requestHandlerLambdaExecutionRole
Here we adding python code directly to the CFT. This python code forms our request-handling Lambda function. Whenever a request is made to the API, this Lambda function will be triggered. As you can see from the code, it returns a json response with a list of links. Error handling is also included in the code, in case something goes wrong. Note the DependsOn
attribute for the resource. This is telling AWS that the requestHandlerLambdaExecutionRole
resource must be built before the requestHandler
resource.
The next resource is the ApplicationAPI
, an APIGateway resource. APIGateway is AWS’s API service.
ApplicationAPI: Type: AWS::ApiGateway::RestApi Properties: Name: ApplicationAPI Description: Simple API for an application DependsOn: - requestHandler
The next resource, LambdaPermission
, gives permission to the API resource (ApplicationAPI
) to invoke our Lambda function:
LambdaPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt - requestHandler - Arn Action: 'lambda:InvokeFunction' Principal: apigateway.amazonaws.com SourceArn: !Join ["", [!Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:", !Ref ApplicationAPI, "/*"]] DependsOn: - ApplicationAPI
Next up is an endpoint for the API, which we will call link
:
link: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApplicationAPI ParentId: !GetAtt [ApplicationAPI, RootResourceId] PathPart: 'link' DependsOn: - ApplicationAPI
Now we add a GET method to our endpoint, ProxyResourceGET
. This handles proxying API link endpoint GET requests to the Lambda function.
ProxyResourceGET: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: GET AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ApplicationAPI
We’ll also add an OPTIONS method to our link endpoint, OptionsMethod
. This won’t be used directly, but is needed in case we want to preform cross-origin API requests. This is where we call our API from a different domain from where it is hosted. This is likely going to be the case, because we are likely to host any website that calls the API in a different location to AWS’s API Gateway domains.
OptionsMethod: Type: AWS::ApiGateway::Method Properties: AuthorizationType: NONE RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: OPTIONS Integration: IntegrationResponses: - StatusCode: 200 ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" ResponseTemplates: application/json: '' PassthroughBehavior: NEVER RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true DependsOn: - ProxyResourceGET
The final resource is Deployment
, which deploys the API to an environment (called a ‘stage’). The stage name here is called ‘prod’, but you can call it anything you like.
Deployment: DependsOn: "OptionsMethod" Type: AWS::ApiGateway::Deployment Properties: RestApiId: Ref: "ApplicationAPI" Description: "Application API deployment" StageName: "prod"
The last section of the CFT is the Outputs
section. This is where we can output any created resource information. In here, we’ll add the API endpoint URL that we’ll need to use to make requests.
Outputs: APIUrl: Description: Application API Gateway link endpoint URL Value: !Join - '' - - 'https://' - !Ref ApplicationAPI - '.execute-api.' - !Ref AWS::Region - '.amazonaws.com/prod/link'
The whole CFT, when all put together, will be:
AWSTemplateFormatVersion: "2010-09-09" Description: Creates a simple API endpoint that returns a list of links Resources: requestHandlerLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: Allow Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" Path: "/" Policies: - PolicyName: "root" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:PutLogEvents - logs:CreateLogStream Resource: "*" RoleName: requestHandlerLambdaExecutionRole requestHandler: Type: "AWS::Lambda::Function" Properties: Code: ZipFile: !Sub | import json def lambda_handler(event, context): try: print(event) print(context) # Because this Lambda function is called by an API Gateway proxy integration # the result object must use the following structure. return { "statusCode": 200, "isBase64Encoded": False, "body": json.dumps({ "links": ["https://www.google.com", "https://www.amazon.com"], }), "headers": { 'Access-Control-Allow-Origin': '*', }, } except Exception as err: print(str(err)) # If there is an error during processing, catch it and return # from the Lambda function successfully. Specify a 500 HTTP status # code and provide an error message in the body. This will provide a # more meaningful error response to the end client. error_response(str(err), context.aws_request_id) def error_response(error_message, aws_request_id): return { "statusCode": 500, "isBase64Encoded": False, "body": json.dumps({ "Error": error_message, "Reference": aws_request_id, }), "headers": { 'Access-Control-Allow-Origin': '*', }, } Description: Lambda function for processing api endpoint requests FunctionName: requestHandler Handler: index.lambda_handler Role: Fn::GetAtt: - "requestHandlerLambdaExecutionRole" - "Arn" Runtime: python3.8 DependsOn: - requestHandlerLambdaExecutionRole ApplicationAPI: Type: AWS::ApiGateway::RestApi Properties: Name: ApplicationAPI Description: Simple API for an application DependsOn: - requestHandler LambdaPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !GetAtt - requestHandler - Arn Action: 'lambda:InvokeFunction' Principal: apigateway.amazonaws.com SourceArn: !Join ["", [!Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:", !Ref ApplicationAPI, "/*"]] DependsOn: - ApplicationAPI link: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref ApplicationAPI ParentId: !GetAtt [ApplicationAPI, RootResourceId] PathPart: 'link' DependsOn: - ApplicationAPI ProxyResourceGET: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: GET AuthorizationType: NONE Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${requestHandler.Arn}/invocations IntegrationResponses: - StatusCode: 200 MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty DependsOn: - ApplicationAPI OptionsMethod: Type: AWS::ApiGateway::Method Properties: AuthorizationType: NONE RestApiId: !Ref ApplicationAPI ResourceId: !Ref link HttpMethod: OPTIONS Integration: IntegrationResponses: - StatusCode: 200 ResponseParameters: method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" method.response.header.Access-Control-Allow-Methods: "'GET,POST,OPTIONS'" method.response.header.Access-Control-Allow-Origin: "'*'" ResponseTemplates: application/json: '' PassthroughBehavior: NEVER RequestTemplates: application/json: '{"statusCode": 200}' Type: MOCK MethodResponses: - StatusCode: 200 ResponseModels: application/json: Empty ResponseParameters: method.response.header.Access-Control-Allow-Headers: true method.response.header.Access-Control-Allow-Methods: true method.response.header.Access-Control-Allow-Origin: true DependsOn: - ProxyResourceGET Deployment: DependsOn: "OptionsMethod" Type: AWS::ApiGateway::Deployment Properties: RestApiId: Ref: "ApplicationAPI" Description: "Application API deployment" StageName: "prod" Outputs: APIUrl: Description: Application API Gateway link endpoint URL Value: !Join - '' - - 'https://' - !Ref ApplicationAPI - '.execute-api.' - !Ref AWS::Region - '.amazonaws.com/prod/link'
Step 2: Launch the CloudFormation Template
Save the CFT to a file called setupapi.yml
. Using the AWS CLI, launch the CFT with the following command:
$ aws cloudformation create-stack --stack-name api --template-body file://setupapi.yml --capabilities CAPABILITY_NAMED_IAM
You can poll the following command until the StackStatus
field in the output changes to “CREATE_COMPLETE”. At that point, the CFT stack has been built.
$ aws cloudformation describe-stacks --stack-name api
Once complete, you’ll also see the API URL in the Outputs
section of the response, next to OutputValue
. It should look something like “https://xxxxxxx.execute-api.x-region-1.amazonaws.com/prod/link”.
Step 3: Test the API
Test the API by navigating to the endpoint URL in your browser. If successful, you should see this response displayed:
{"links": ["https://www.google.com", "https://www.amazon.com"]}
Step 4: Clean Up
If you don’t want to keep your API (note that AWS will charge you for it if you don’t have free tier on your account), delete the CFT stack (all resources created) by running:
$ aws cloudformation delete-stack --stack-name api
You will then need to remove any logs that were generated by the Lambda function. To do this:
- Log into the AWS Console (your AWS account) using your Administrator IAM user, and navigate to CloudWatch.
- On the left-hand side, select Logs -> Log Groups.
- Select ‘aws/lambda/requestHandler’.
- Under Actions, select ‘Delete log groups’ and confirm.
Conclusion
You should now be able to set up your own simple API on AWS in minutes. If you want to learn about adding a DynamoDB database to your API, read my follow-on post Creating an API with AWS: Part 2: DynamoDB. Thanks for reading, see you next time!
2 Responses
[…] Creating an API with AWS November 6, 2021 […]
[…] you how I set up a simple API using python’s FastAPI, with five GET endpoints. If you read my Creating an API with AWS series, you may recognise the endpoints. My eventual aim is to recreate the API I built in AWS […]