<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Ben Giddins on Medium]]></title>
        <description><![CDATA[Stories by Ben Giddins on Medium]]></description>
        <link>https://medium.com/@bengiddins?source=rss-f02a99b0a42d------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/2*hvnRZlM-KaW2AQLMpUsv5g.png</url>
            <title>Stories by Ben Giddins on Medium</title>
            <link>https://medium.com/@bengiddins?source=rss-f02a99b0a42d------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 06:04:57 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@bengiddins/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[GraphQL and Go with AppSync and Lambda]]></title>
            <link>https://medium.com/@bengiddins/graphql-and-go-with-appsync-and-lambda-84bac32582cd?source=rss-f02a99b0a42d------2</link>
            <guid isPermaLink="false">https://medium.com/p/84bac32582cd</guid>
            <category><![CDATA[graphql]]></category>
            <category><![CDATA[aws-appsync]]></category>
            <category><![CDATA[aws-lambda]]></category>
            <category><![CDATA[go]]></category>
            <dc:creator><![CDATA[Ben Giddins]]></dc:creator>
            <pubDate>Tue, 08 Oct 2019 21:28:28 GMT</pubDate>
            <atom:updated>2019-10-08T21:28:28.672Z</atom:updated>
            <content:encoded><![CDATA[<p>At Rumple we’ve been working on building our serverless precious metals trading platform using React, GraphQL and Go, deployed on AWS. Our entire infrastructure (except for a WordPress website!) is serverless — nary an EC2 instance to be seen. The language of choice for our Lambda development is Go — it was selected for its rising popularity, familiarity of development concepts (I spent the 90s and 2000s working with C and Java), and at the time, leaderboard performance on AWS Lambda as a compiled language that didn’t have the overhead of loading a JVM. Plus I didn’t know Node.js, so there was that.</p><p>Until this project, I had only worked with REST APIs — GraphQL was a brand new thing. After running through the Apollo tutorials to get up to speed, I started looking at AWS AppSync, and the outlines of an architecture started to come together — React and React Native UX layers, communicating with GraphQL through AppSync to Lambda resolver functions that fronted an event driven backend, all implemented in Lambda. Throw in some Cognito for security, and we were off and racing. I did work on a side project that used Apollo GraphQL outside of AWS in the interim, but for our purposes, AWS is excellent.</p><p>During what has turned out to be a very long build phase (we’re in a FinCEN regulated industry, which doesn’t really lend itself to moving fast and breaking things), a design pattern has emerged for using Lambda Go functions with GraphQL. I genuinely hope that there’s something here you can use as a basis for your own projects.</p><p>The power of a Lambda function for a GraphQL resolver is the <strong>switch </strong>statement — a single resolver can service multiple queries or mutations related to a particular entity. For example, related tasks like CRUD functions for address management can be handled by a single Lambda function that resolves actions like <strong>createAddress</strong>, <strong>readAddress</strong>, <strong>updateAddress </strong>and <strong>deleteAddress</strong>. All that is required is that an <strong>AddressRequest </strong>input type in GraphQL contains all of the required parameters to manage these requests, and an <strong>AddressResponse </strong>type contains all of the required attributes to return the necessary data. Once your input or response types start getting too complex, you know its indicative of time to split the function up to be more granular. At the end of the day though, it means though is that if you have 100 queries and mutations in your schema, you can have far fewer than 100 Lambda functions to service it. Much easier to manage.</p><p>Let’s look at some examples. This is what a project directory structure looks like for illustrative purposes:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/176/1*SLw-Hue5mMrLGqF8UJXQwA.png" /></figure><p>Our GraphQL approach is as follows:</p><ul><li>GraphQL APIs separated by security domain— our client and admin APIs use separate Cognito pools for authentication, so we split them in two (only our client API is shown above)</li><li>GraphQL schema, data sources and resolvers all managed in Terraform for source control</li><li>queries and mutations use a <strong>Request</strong> <strong>input </strong>and <strong>Response</strong> <strong>type </strong>signature, where the request and the response structures contain all of the parameters and attributes for several related queries and mutations</li><li>enum lists are used to enforce sets of values, e.g. if a trade’s direction can only be BUY or SELL, an enum will be declared with just those two valid values — a trade type of QUOTE would be rejected</li></ul><p>Our client GraphQL schema in client.graphql might look like the following:</p><pre>schema {<br>    query: Query<br>    mutation: Mutation<br>}</pre><pre>type Query {<br>    getAddressList: AddressResponse<br>    readAddress(input: AddressRequest): AddressResponse<br>}</pre><pre>type Mutation {<br>    createAddress(input: AddressRequest): AddressResponse<br>    updateAddress(input: AddressRequest): AddressResponse<br>    deleteAddress(input: AddressRequest): AddressResponse<br>}</pre><pre>enum AddressType {<br>    RESIDENTIAL<br>    BILLING<br>    SHIPPING<br>    MAILING<br>}</pre><pre>enum Channel {<br>    MOBILE<br>    WEB <br>}</pre><pre>input AddressRequest {<br>    uuid: String<br>    line_1: String<br>    line_2: String<br>    city: String<br>    zip_postcode: String<br>    state_province: String<br>    country: String<br>    address_type: AddressType<br>    channel: Channel!<br>}</pre><pre>type Address {<br>    uuid: String<br>    line_1: String<br>    line_2: String<br>    city: String<br>    zip_postcode: String<br>    state_province: String<br>    country: String<br>    address_type: String<br>}</pre><pre>type AddressResponse {<br>    addresses: [Address]<br>    error: Error<br>}</pre><pre>type Error {<br>    message: String<br>}</pre><p>We’ve defined two queries and three mutations for our CRUD functions, two enumerators for the Address type and the UI channel we receive an invocation from, an input type for receiving an address or an address identifier, a response type for passing back one or more addresses, and an error type to allow clean error handling on the client.</p><p>These GraphQL query names, mutation names inputs, types and enums are then modeled in Go as <strong>consts</strong>,<strong> structs</strong>,<strong> </strong>and <strong>custom types</strong>. We define a local <strong>graphql </strong>package to hold these definitions — if a request or response type needs to change in GraphQL, we only need to update one struct in Go to mirror the change, along with any logic edits to support the change.</p><p>Over in Go, we represent the same schema now using Go types in graphql.go:</p><pre>package graphql</pre><pre>// Enumeration types<br>type AddressType string<br>type Channel string</pre><pre>// Constants<br>const (<br>    // Queries<br>    GetAddressList = &quot;getAddressList&quot;<br>    ReadAddress    = &quot;readAddress&quot;</pre><pre>    // Mutations<br>    CreateAddress = &quot;createAddress&quot;<br>    UpdateAddress = &quot;updateAddress&quot;<br>    DeleteAddress = &quot;deleteAddress&quot;</pre><pre>    // Enumerators<br>    Residential AddressType = &quot;RESIDENTIAL&quot;<br>    Billing     AddressType = &quot;BILLING&quot;<br>    Shipping    AddressType = &quot;SHIPPING&quot;<br>    Mailing     AddressType = &quot;MAILING&quot;</pre><pre>    Web    Channel = &quot;WEB&quot;<br>    Mobile Channel = &quot;MOBILE&quot;<br>)</pre><pre>// Input<br>type AddressRequest struct {<br>    UUID          string      `json:&quot;uuid&quot;`<br>    Line1         string      `json:&quot;line_1&quot;`<br>    Line2         string      `json:&quot;line_2&quot;`<br>    City          string      `json:&quot;city&quot;`<br>    ZipPostcode   string      `json:&quot;zip_postcode&quot;`<br>    StateProvince string      `json:&quot;state_province&quot;`<br>    Country       string      `json:&quot;country&quot;`<br>    AddressType   AddressType `json:&quot;address_type&quot;`<br>    Channel       Channel     `json:&quot;channel&quot;`<br>}</pre><pre>type Address struct {<br>    UUID          string       `json:&quot;uuid&quot;`<br>    Line1         string       `json:&quot;line_1&quot;`<br>    Line2         string       `json:&quot;line_2&quot;`<br>    City          string       `json:&quot;city&quot;`<br>    ZipPostcode   string       `json:&quot;zip_postcode&quot;`<br>    StateProvince string       `json:&quot;state_province&quot;`<br>    Country       string       `json:&quot;country&quot;`<br>    AddressType   *AddressType `json:&quot;address_type&quot;` // may be null<br>}</pre><pre>type AddressResponse struct {<br>    Addresses []Address `json:&quot;addresses&quot;`<br>    Error       Error     `json:&quot;error&quot;`<br>}</pre><pre>type Error struct {<br>    Message *string `json:&quot;message&quot;`<br>}</pre><p>GraphQL enum lists are added as custom types (usually a custom string type). By using these custom types in the structs, we ensure that Go will only return valid set values expected by GraphQL. Note that as an empty string in Go is not null, if the data schema allows for null values in an attribute, we should use a pointer to a custom type in a struct to signify that nulls are allowed. GraphQL cannot handle an empty string if it’s expecting an enum entry, but it can handle a null. For example, we also return a null error message if there is no error, so we need to use a pointer to a string in our Error struct, otherwise it would return empty strings rather than null.</p><p>If you have two or more entries in GraphQL enums that share the same value (e.g. NEW might be a valid state for both a trade status and a customer status), you will need to create multiple packages in Go for the schema replication, as a Go package cannot contain duplicate constant names even if they are of different types. In this instance, we would remove both the TradeStatus and CustomerStatus custom types from our graphql package, and create a trade package and customer package where they would reside. This ensures that there is no ambiguity, while striving to maintain a central definition of the GraphQL schema using Go types.</p><pre>package trade                        package customer</pre><pre>type State string                    type State string<br>const (                              const (<br>    New State = &quot;NEW&quot;                    New State = &quot;NEW&quot;<br>}                                    }</pre><p>These could then be referenced as trade.New and customer.New without conflict.</p><p>Rumple uses HashiCorp’s Terraform for IaC management. Terraform allows complete management of a Cognito / AppSync / Lambda deployment, but for brevity, let’s look at just the relevant parts that tie our GraphQL schema and Lambda functions together:</p><pre>resource &quot;<strong>aws_lambda_function</strong>&quot; &quot;client_address&quot; {<br>  # Function code<br>  function_name    = &quot;client-address&quot;<br>  description      = &quot;Resolver for client address GraphQL operations&quot;<br>  filename         = &quot;lambda/address.zip&quot;<br>  runtime          = &quot;go1.x&quot;<br>  handler          = &quot;address&quot;<br>  source_code_hash = &quot;${base64sha256(file(&quot;lambda/address.zip&quot;))}&quot;</pre><pre># Environment variables<br>  environment {<br>    variables = {<br>      DB = &quot;${aws_secretsmanager_secret.database_password.name}&quot;<br>    }<br>  }</pre><pre># Tags<br>  tags = {<br>    Terraform   = &quot;true&quot;<br>    Usage = &quot;GraphQL resolver&quot;<br>  }</pre><pre># Execution role<br>  role = &quot;${aws_iam_role.lambda_execution.arn}&quot;</pre><pre># Basic settings<br>  memory_size      = &quot;128&quot;<br>  timeout          = &quot;10&quot;</pre><pre># Network configuration<br>  vpc_config {<br>    subnet_ids         = [&quot;${module.vpc.private_subnets}&quot;]<br>    security_group_ids = [&quot;${aws_security_group.outbound.id}&quot;]<br>  }<br>}<br>resource &quot;<strong>aws_appsync_datasource</strong>&quot; &quot;client_address&quot; {<br>  api_id           = &quot;${aws_appsync_graphql_api.client.id}&quot;<br>  name             = &quot;client_address&quot;<br>  type             = &quot;AWS_LAMBDA&quot;<br>  service_role_arn = &quot;${aws_iam_role.appsync_invoke_lambda.arn}&quot;</pre><pre>lambda_config {<br>    function_arn = &quot;${aws_lambda_function.client_address.arn}&quot;<br>  }<br>}</pre><pre>resource &quot;<strong>aws_appsync_resolver</strong>&quot; &quot;get_address_list&quot; {<br>  api_id           = &quot;${aws_appsync_graphql_api.client.id}&quot;<br>  field            = &quot;getAddressList&quot;<br>  type             = &quot;Query&quot;<br>  data_source      = &quot;${aws_appsync_datasource.client_address.name}&quot;<br>  request_template = &lt;&lt;EOF<br>  #set( $myMap = {<br>          &quot;field&quot;: &quot;getAddressList&quot;,<br>          &quot;user_id&quot;: $context.identity.sub,<br>          &quot;arguments&quot;:  $context.arguments<br>  } )<br>  {<br>      &quot;version&quot; : &quot;2017-02-28&quot;,<br>      &quot;operation&quot;: &quot;Invoke&quot;,<br>      &quot;payload&quot;: $util.toJson($myMap)<br>  }<br>EOF<br>  response_template = &lt;&lt;EOF<br>  $util.toJson($context.result)<br>EOF<br>}</pre><p>Here we’ve defined a function, a data source and a resolver in Terraform. The data source refers to our Lambda function, and there can be <em>n </em>resolvers per data source.</p><p>If you’re not using Terraform, you would configure this through the AppSync and Lambda web consoles.</p><p>Note that the request template is passing the following values:</p><pre>&quot;field&quot;: &quot;getAddressList&quot;,<br>&quot;user_id&quot;: $context.identity.sub,<br>&quot;arguments&quot;:  $context.arguments</pre><p>These correlate to the parameters received by the Lambda function’s request struct — a <strong>field</strong>, which is the query or mutation name, the <strong>user_id </strong>of the authenticated Cognito user (which is a UUID string), and the <strong>arguments </strong>parameter containing JSON that reflects the <strong>AddressRequest </strong>type. The case here must match the names within the JSON tags of the Go struct.</p><p>Now let’s look inside the Go Lambda function.</p><pre>// Structure for an incoming GraphQL request<br>// Matches the request_template in aws_appsync_resolver<br>type requestObj struct {<br>    Field     string       `json:&quot;field&quot;`<br>    UserId    string       `json:&quot;user_id&quot;`<br>    Arguments argumentsObj `json:&quot;arguments&quot;`<br>}</pre><pre>// Structure for arguments received from GraphQL<br>type argumentsObj struct {<br>    Input graphql.AddressRequest `json:&quot;input&quot;`<br>}</pre><p>We have our expected parameters in the <strong>requestObj </strong>struct (matching the resolver’s request template), and we have an <strong>argumentsObj </strong>that contains all of the parameters passed to the GraphQL query or mutation in a parameter of type AddressRequest (defined in graphql.go).</p><p>Further down in the Lambda function, we reach our Handler:</p><pre>func Handler(ctx context.Context, req requestObj) (interface{}, error) {<br>    // Local variables<br>    var res graphql.AddressResponse<br>    var err error</pre><pre>// Process GraphQL request<br>    switch req.Field {<br>    case graphql.GetAddressList:<br>        err = getAddressList(&amp;req, &amp;res)<br>    case graphql.ReadAddress:<br>       err = readAddress(&amp;req, &amp;res)<br>    case graphql.CreateAddress:<br>       err = createAddress(&amp;req, &amp;res)<br>    case graphql.UpdateAddress:<br>       err = updateAddress(&amp;req, &amp;res)<br>    case graphql.DeleteAddress:<br>       err = deleteAddress(&amp;req)<br>    default:<br>       // Unknown invocation<br>       errorMsg := &quot;Handler() unknown GraphQL &quot; + req.Field<br>       err = errors.New(errorMsg)<br> }</pre><pre>    // Inspect for errors<br>    if err != nil {<br>        errorMsg := err.Error()<br>        res.Error.Message = &amp;errorMsg<br>    }</pre><pre>    // Ensure empty slices, not nulls returned<br>    if res.Addresses == nil {<br>        res.Addresses = []graphql.Address{}<br>    }</pre><pre>    // We don&#39;t return errors, we return an error message<br>    return res, nil<br>}</pre><p>This is where the pattern comes together. The switch statement in the Handler examines the field parameter of all resolver calls made to the data source that this Lambda function is configured for. So in our address example, five GraphQL queries and mutations, one Lambda function. The appropriate function is called and passed the request object, and if the function needs to return data, is also passed a result object that it can modify.</p><p>All error handling is done once in the Handler() function.</p><p>Over in our React app, we define our queries and mutations as constants. I like to put these all into a single JSX file that holds related actions, and import them as needed where being used.</p><pre>export const ADDRESS_LIST = `<br>query AddressList {<br>    <strong>getAddressList </strong>{<br>        addresses {<br>            uuid<br>            line_1<br>            line_2<br>            city<br>            zip_postcode<br>            state_province<br>            country<br>            address_type<br>        }<br>        error {<br>            message<br>        }<br>    }<br>}`;</pre><pre>...</pre><pre>export const DELETE_ADDRESS = `<br>mutation DeleteAddress($input: AddressRequest!) {<br>    <strong>deleteAddress</strong>(input: $input) {<br>        error {<br>            message<br>        }<br>    }<br>}`;</pre><p>The link between this schema definition in React and the resolvers in AppSync is the name of the query or mutation (bolded) inside of the query definition. This will match the query or mutation in the GraphQL schema uploaded to AppSync, and match the value of the const defined in Go.</p><p>I picked these two for illustration — getAddressList does not need to pass any invocation-specific parameters to the Lambda function (the current user_id is passed through the resolver mapping), so there is no input parameter. A call to it in React might look like:</p><pre>import { ADDRESS_LIST } from &#39;schema/address&#39;;</pre><pre>...</pre><pre>API.graphql(graphqlOperation(<strong>ADDRESS_LIST</strong>)).then((response) =&gt; {<br>...</pre><p>Conversely, deleteAddress needs to know which address is being deleted, so it might receive a JSON input parameter that looks like this:</p><pre>import { DELETE_ADDRESS} from &#39;schema/address&#39;;</pre><pre>...</pre><pre>let myInput = {};<br>myInput.uuid = this.state.addressUUID;</pre><pre>API.graphql(graphqlOperation(<strong>DELETE_ADDRESS, {input: myInput}</strong>)).then((response) =&gt; {<br>...</pre><p>The deleteAddress mutation will receive the JSON representation of the AddressRequest type, with just the uuid parameter populated.</p><p>Try to rationalize the number of parameters in your return types — for example, getAddressList needs to return <em>n</em> addresses, while readAddress only needs to return 1. readAddress can return a single address in the addresses slice at addresses[0] rather than having its own standalone attributes in the response. A success condition check might look like the following:</p><pre>import { READ_ADDRESS} from &#39;schema/address&#39;;</pre><pre>...</pre><pre>let myInput = {};<br>myInput.uuid = this.state.addressUUID;</pre><pre>API.graphql(graphqlOperation(READ_ADDRESS, {input: myInput})).then((response) =&gt; {<br>    if (response.data &amp;&amp; response.data.getAddressList &amp;&amp; response.data.getAddressList.addresses &amp;&amp; <strong>response.data.getAddressList.addresses.length === 1</strong>) {<br>        // null safe inspection was successful, address found<br>    }<br>}</pre><p>The syntax is verbose, but this checking prevents attempting to evaluate attributes of an undefined value.</p><p>When returning responses from mutations like a save or update operation, it’s good practice to always return the item that was being mutated. For deletions, just return an error, which will be null if successful, or populated if unsuccessful.</p><p>As a practice, I return nulls in GraphQL if an attribute is empty, rather than an empty string. This can be seen in how we process errors in Go’s handler:</p><pre>type Error struct {<br>    Message *string `json:&quot;message&quot;` // null by default<br>}</pre><pre>...</pre><pre>// Inspect for errors<br>if err != nil {<br>    errorMsg := err.Error()<br>    res.Error.Message = &amp;errorMsg<br>}</pre><p>…and how we receive errors in React:</p><pre>API.graphql(graphqlOperation(DELETE_ADDRESS, {input: myInput})).then((response) =&gt; {<br>...<br>    if (response.data &amp;&amp; response.data.error &amp;&amp; <strong>response.data.error.message</strong>) {<br>        // non-null error received, handle it<br>        alert(response.data.error.message);<br>    }<br>}</pre><p>If there is no error, this evaluation will fail, as response.data.error.message will be null, and processing will continue.</p><p>I also return empty arrays rather than null arrays, just in case there’s client code that is not null safe when handling an array.</p><p>Obviously there is a lot more to writing a Lambda function than just the Handler() method, but we can cover that in a future blog post.</p><p>Hope this gets you started!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=84bac32582cd" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>