This is a multi-part series on using C#, AWS Lambda, and Serverless Framework to build REST services, ChatBots, Alexa Skills and more
This week, we’ll explore creating HTTP endpoints that execute your Lambda functions. I’ll assume that you have the starter template from the first post.
Up until now, our function has lived in relative isolation. The only way we’ve been able to call the
hello function has been using
serverless invoke which translates to a direct AWS API call, or by using the AWS console to test the function directly. Unsurprisingly, these are not the intended interaction models for AWS Lambda functions.
In AWS Lambda services, your functions will typically execute in response to various types of events. These can be things such as “a file was uploaded to S3”, “a server crashed”, “an item was added to your DynamoDB database”, or even “an HTTP request was made to this API Gateway endpoint”. In this post, you’ll see how you can configure respond to the latter, an HTTP endpoint.
Because Lambda functions can be triggered by many types of events, it’s worthwhile to highlight that neither the AWS Lambda infrastructure nor your function are aware of networking or HTTP. Functions simply take in a stream of data and output a stream of data (though as we’ll see, serialization plays a part in simplifying this interface). The event sources determine the schema of the input data they provide to the function as well as the output schema they expect back from the function (if any).
The event source used for responding to HTTP events is AWS’s API Gateway. API Gateway is a service for exposing HTTP endpoints that map to any number of your AWS resources that you want those endpoints to execute. In our case, we’ll be triggering AWS Lambda functions and returning their output.
The lifecycle of a web request looks roughly like this:
- A client makes a
/helloworldwhich is configured a API Gateway endpoint
GETmethod for the
/helloworldresource is mapped to the
- API Gateway wraps up data about the
GETrequest in a json payload which contains the headers, HTTP method, full request path, request body, and other HTTP metadata
hellofunction parses that input and decides how to respond
hellofunction returns a json payload with the status code, headers, and the response body back to API Gateway
- API Gateway formats that response and returns it as a proper HTTP response back to the client
Using Serverless Framework, we won’t have to worry about many of the particulars of this flow.
serverless.yml, we have the following for our function configuration:
What we’re going to add to his is the
events property for our
hello function. Change this section to look like the following:
Now that we’ve modified our service definition (
serverless.yml), let’s remember what we need to do to deploy it. Note that since we haven’t made modifications to the C#, we don’t need to build an updated package.
When that command completes, you should see some info in the “Service Information” output that wasn’t populated before (your subdomain will be different):
Neat! Here we see the API Gateway endpoint that
serverless generated which should map to your function. Copy your URL (not the one posted above) and navigate to it in your browser. You’ll likely see the following:
What gives? Well, the good news is that we didn’t get a browser error page, so we’re probably doing something right. Maybe the problem is with our function? Let’s see if our function works in API Gateway.
Since our HTTP resource,
/dev/helloworld, is managed by API Gateway, we can look for a point of failure there. Log in to the AWS Console, and navigate to the “API Gateway” service.
In API Gateway, you’ll see
dev-foo listed under the
APIs section. An “API” groups all the endpoints of your serverless service. If you click on
dev-foo, you’ll see that we have one,
/helloworld which supports the
GET HTTP method. Clicking on
/helloworld brings us to the pipeline diagram for that specific action. It describes the request/response lifecycle of our endpoint:
- A client makes a HTTP request
- The request approaches the configured integration and is transformed to the format the integration prefers
- An API integration is the thing that actually handles the request, in our case, a Lambda function
- The integration receives the transformed request and performs some logic with it
- The integration returns a response to API Gateway
- The integration’s response is transformed into a HTTP response
- The client receives the HTTP response
There’s a lot of opportunities for misconfiguration in that pipeline. Thankfully, API Gateway makes it easy to test resources. Click on “Test” on the “Client” box on the left. You’ll see a page that allows you to try out making requests to your resource with different query strings, headers, and request bodies (for supported HTTP verbs).
Since we’re not doing anything special in our function with query strings or headers yet, leave everything blank and click “Test”.
On the right side of the screen, you’ll see that we still the get the
"Internal server error" message, but more helpfully, we have logs for all parts of the request pipeline.
First, there’s a lot of information about the request, such as our empty body, headers and the like. Then, it shows the
body after transformation which is the full batch of data which our Lambda function receives.
Surprisingly, we then see the response body we expected with the
Go Serverless message. Looks like our function didn’t fail after all. What did fail is the next step. A couple lines down, we see the following message:
Hrm. Looks like we didn’t format the output of our Lambda function the way that API Gateway was expecting. Diving deep in to the AWS documentation, it was expecting a response of the following format:
To model the function output the way that API Gateway expects, we’ll need to start making some code changes. Yay!
Delete the existing
Response class and add the following class:
Here we see the three required properties represented: “statusCode”, “headers”, and “body”.
One thing to clarify is the use of the
JsonProperty attribute. API Gateway expects
camelCase for all property names. However, idiomatic C# uses
PascalCase for public properties. Because the Lambda Serializer is based on Json.NET, we can use
JsonProperty tell the serializer to do two things:
- When serializing the C# object with the
Bodyproperty, name the json property
- When deserializing a json object, look for the property
bodyand use it to populate the
Bodyproperty in C#
In a future post, we’ll go into more detail about serialization and look at easier ways to handle the mapping between idiomatic C# and idiomatic json.
To set the new response model, modify the
Hello method as follows:
Jump back to the command line and build and deploy the modified function:
Try the endpoint URL in the browser again. You should now see the
Go Serverless! Hello from HTTP message. Yay!
But wait, what happened to the structure of our original message?
I cheated a little bit in the last code change to get the HTTP request working. Let’s get back to the original response data schema. Add the following class below the definition of
Also modify the
Hello method again:
What’s going on here? Well, remember that the
body property of the response must be of type
string? That means that we have to explicitly serialize our response body as a string.
serverless deploy, try your function in the browser again and you should get the following (though not as nicely formatted):
Neat! Now we know how to massage and structure our responses so that API Gateway can handle them. Since the move to using HTTP events, though, our example
Request type hasn’t made a lot of sense. The data that comes through from the browser’s request and the API Gateway transformation doesn’t have any of
Key3, so those properties aren’t populated when Lambda attempts to serialize them.
So what data can we use in our Lambda function? Since
GET requests don’t have request bodies, let’s use query parameters.
What will that look like? Instead of digging though the deep waters of AWS documentation, recall that the API Gateway resource test page logged what the transformed integration input looked like. If we add a query string to the test input, we can see what or function would get.
Go to the API Gateway test page again and put
name=Ada in the “Query String” field, then click “Test”. Take a look at the output labeled “Endpoint request body after transformations”:
From this, it looks like it’ll be pretty straightforward to create a new
Request class that models the integration request. Replace the exising
Request class with the following:
Note that we don’t have to have a property for everything we receive. All other properties will be silently ignored. With this
Request class in place, modify the
Hello method one last time to look like this:
We want to default the name and not error if one is not provided. We saw earlier that
queryStringParameters is null if there is no query string, so we need to check against that.
After one last
serverless deploy, try your function out. You can test it like we were before, or you can add
?name=Ada, and you’ll see a response like this:
Once again, well done! Next post, we’ll extend our function to handle HTTP POSTs and build a fully-functioning chat bot!