AWSAppSync | 12 Min Read

Integrating Environment Variables with AWS AppSync via the AWS CDK

Learn how to integrate environment variables in a GraphQL API using AWS AppSync, deployed via the AWS CDK in this comprehensive guide.

Recently there was an announcement that AWS AppSync now supports environment variables on their GraphQL APIs. However, the documentation provided for doing it doesn’t showcase how to do it using the AWS CDK.

So, in this tutorial, we’re going to take a look at how to create a GraphQL API using AppSync via the AWS CDK that will allow us to create and retrieve items from a DynamoDB database. And, then when defining the API via the AWS CDK, we’re going to use the new environment variables feature to retrieve secrets in our JavaScript resolvers which we’ll then use when creating and retrieving items from the database.

So, let’s not waste any time and get straight into the post!

Prerequisites

There aren’t many prerequisites for this tutorial, the main two are that you will require an AWS account already set up and configured. Secondly, you will require a CDK project setup and created on your local machine which if you don’t already have you can create by running the command `cdk init app --language typescript`.

Defining a DynamoDB Table

Once you have your CDK stack created, the first thing we’re going to want to do is to create a new DynamoDB table definition in the stack definition file in the `lib` directory. You can do this by adding the code below.

./lib/*-stack.ts
1// Create a new DynamoDB table
2const table = new Table(this, "appsync-envs-table", {
3 partitionKey: { name: "id", type: AttributeType.STRING },
4 removalPolicy: cdk.RemovalPolicy.DESTROY,
5});
ts

Defining the GraphQL API

With our database definition sorted, we can now turn our attention to the GraphQL API itself. To define this in our CDK stack, we’re going to use the `CfnGraphQLApi` L1 construct. This is because currently at the time of writing the more commonly used `GraphqlApi` construct doesn’t support the `environmentVariables` property.

To add this definition to our stack, add the below code under the code we just added for our DynamoDB database.

./lib/*-stack.ts
1// Create a new GraphQL API using CfnGraphQLApi as GraphQLApi does not support environment variables yet
2const api = new CfnGraphQLApi(this, "appsync-envs-graphql-api", {
3 name: "appsync-envs-api",
4 environmentVariables: {
5 // Define any environment variables to be added to the API
6 secret: "some-secret-value",
7 },
8 authenticationType: "API_KEY",
9});
ts

In this code, we create our API but we also add some configuration options. Firstly, we specify that the `authenticationType` we’d like to use is `API_KEY` (we’ll create our API key in a moment). Secondly, and more importantly for this tutorial, we define the `environmentVariables` we’d like to add to our GraphQL API. In this case, we only add one example value called `secret` with a value of `some-secret-value`.

Creating an API Key

As mentioned a moment ago, we’re now going to create the API key to use with the GraphQL API we just defined. To do this, add the below code under the code we just added for the GraphQL API.

./lib/*-stack.ts
1// Create a new API key for our GraphQL API
2const apiKey = new CfnApiKey(this, "appsync-envs-api-key", {
3 apiId: api.attrApiId,
4 // Time expires time is based on epoch time
5 expires: 1741162545,
6});
ts

Not too much is happening in this code, we define the API Key itself and then we link it to the API that we want to use it with. Finally, we then add the time we want the API key to expire.

NOTE: In this case I’ve set the API key to expire on 5th March 2025. But, if you’re reading this tutorial after that date or want to experiment with different values. Use this website to generate a new Epoch timestamp.

Adding our GraphQL Schema

With our GraphQL API defined and our API key added to it, there is only one thing left for us to do on the API definition before we can move on to writing the GraphQL resolvers and that’s add the schema file.

To add the schema, create a new file at `./graphql/schema.graphql` with the below contents.

./graphql/schema.graphql
1type Mutation {
2 createItem: Item
3}
4
5type Query {
6 getItem(id: ID!): Item
7}
8
9type Item {
10 id: ID!
11 value: String
12}
graphql

You can see this is a pretty basic schema which is all we need for this tutorial as we’re only focused on the environment variables.

To link this schema file to our GraphQL API, add the below code to the bottom of your stack definition file in the `lib` directory.

./lib/*-stack.ts
1// Add the schema to our API
2new CfnGraphQLSchema(this, "appsync-envs-schema", {
3 apiId: api.attrApiId,
4 definition: readFileSync("./graphql/schema.graphql", "utf-8"),
5});
ts

With that code now added, we’ve finished everything we need to define the API itself. We can now move on to creating the resolvers in the next section.

Creating our GraphQL Resolvers

When it comes to creating our GraphQL resolvers, we’re going to be using TypeScript files compiled out to JavaScript. If this concept is new to you or if you would like to read in more detail about it, check out my full tutorial covering it.

Also, at this point, you’ll need to install a few NPM packages that we’ll be using throughout this section of the tutorial. You can do this by running the command `npm i -D esbuild glob @aws-appsync/utils`.

Creating a New Data Source

The first thing we need to do before we can write our GraphQL resolvers is to add a new data source that will allow us to connect to our DynamoDB database from inside the resolvers. To create this data source, add the below code under the schema file code we added in the last section.

./lib/*-stack.ts
1// Create a new IAM role for our DB table for AppSync to use
2const dataSourceRole = new Role(this, "appsync-envs-datasource-role", {
3 assumedBy: new ServicePrincipal("appsync.amazonaws.com"),
4});
5
6// Attach the required DynamoDB permissions to our role
7dataSourceRole.addToPolicy(
8 new PolicyStatement({
9 actions: [
10 "dynamodb:Query",
11 "dynamodb:GetItem",
12 "dynamodb:Scan",
13 "dynamodb:PutItem",
14 "dynamodb:UpdateItem",
15 "dynamodb:DeleteItem",
16 ],
17 resources: [table.tableArn],
18 })
19);
20
21// Create a new data source to link our API and DB together
22const dataSource = new CfnDataSource(this, "appsync-envs-data-source", {
23 apiId: api.attrApiId,
24 name: "appsyncEnvsDataSource",
25 type: "AMAZON_DYNAMODB",
26 dynamoDbConfig: {
27 tableName: table.tableName,
28 awsRegion: this.region,
29 },
30 serviceRoleArn: dataSourceRole.roleArn,
31});
ts

In this code, we do a few things, we first create a new IAM role that is assumed by AppSync. We then create a new `PolicyStatement` on that role that adds the various DynamoDB actions we might want to perform.

We then create the data source, connecting it to both our DynamoDB table and our GraphQL API while also providing the ARN of the new IAM role we created.

Writing Our Resolvers

With our data source now configured let’s turn our attention to the resolvers themselves. To create these, create two new files at `./graphql/resolvers/ts/create-item.ts` and `./graphql/resolvers/ts/get-item.ts`.

Then inside the `create-item.ts` file, add the below code.

./graphql/resolvers/ts/create-item.ts
1import { Context } from "@aws-appsync/utils";
2import { put } from "@aws-appsync/utils/dynamodb";
3
4export function request(ctx: Context) {
5 const id = util.autoId();
6
7 // Create a new item in our DB using the secret from the environment variables stored in our API
8 return put({
9 key: { id },
10 // Access the environment variable using `ctx.env`
11 item: { value: ctx.env.secret, id },
12 });
13}
14
15export function response(ctx: Context) {
16 return ctx.result;
17}
ts

This file is a rather normal resolver for a AppSync GraphQL API but it does have one notable difference that we’re especially interested in for this tutorial. That difference is the value we assign to the `value` property, `ctx.env.secret`.

If you remember back to the API definition we created at the start of this tutorial, we defined our environment variables for the API. In our case, we only defined one variable, called `secret` and the code above is how we access that variable in our resolver.

AppSync automatically exposes our environment variables under the `env` property of the context (`ctx`) in each request. This means we can fetch our variables from there and use them in our resolvers as we see fit! How cool is that! 😎

Then with our first resolver created, we just need to create the second one. To do that, add the below code to the file `get-item.ts`.

./graphql/resolvers/ts/get-item.ts
1import { Context } from "@aws-appsync/utils";
2import { get } from "@aws-appsync/utils/dynamodb";
3
4export function request(ctx: Context) {
5 return get({
6 key: { id: ctx.args.id },
7 });
8}
9
10export function response(ctx: Context) {
11 return ctx.result;
12}
ts

Compiling The Resolvers To JS

I won’t go into too much detail on the compiling process in this post as I covered it in a lot more detail in the tutorial I mentioned earlier. But, in short, we can’t deploy our TS resolvers to AppSync so we need to compile them out to JS first and then deploy the JS versions.

To compile our resolvers, create a new file in the root of the project called `build.mjs` and add the below code to it.

./build.mjs
1import { build } from "esbuild";
2import { glob } from "glob";
3
4const files = await glob("graphql/resolvers/ts/**/*.ts");
5
6await build({
7 sourcemap: "inline",
8 sourcesContent: false,
9 format: "esm",
10 target: "esnext",
11 platform: "node",
12 external: ["@aws-appsync/utils"],
13 outdir: "graphql/resolvers/js",
14 entryPoints: files,
15 bundle: true,
16});
js

Then update your `package.json` file to include the below two scripts.

./package.json
1"predeploy": "node build.mjs",
2"deploy": "cdk deploy"
json

And, then if you would like to test if the compiling works correctly, run the command `npm run predeploy` in your terminal and your new JS resolvers should be created in the `./graphql/resolvers/js` directory.

Defining The Resolvers

With the resolvers now created and compiled, all that we need to do is to define them in our stack definition file in the `lib` directory. To do that, add the code below under the code we added for the data source in the previous section.

./lib/*-stack.ts
1// Create a new JS resolver (built from the TS file) for our API
2const createItemResolver = new CfnResolver(
3 this,
4 "appsync-envs-create-item-resolver",
5 {
6 apiId: api.attrApiId,
7 typeName: "Mutation",
8 fieldName: "createItem",
9 dataSourceName: dataSource.name,
10 runtime: {
11 name: "APPSYNC_JS",
12 runtimeVersion: "1.0.0",
13 },
14 code: readFileSync("./graphql/resolvers/js/create-item.js", "utf-8"),
15 }
16);
17
18const getItemResolver = new CfnResolver(
19 this,
20 "appsync-envs-get-item-resolver",
21 {
22 apiId: api.attrApiId,
23 typeName: "Query",
24 fieldName: "getItem",
25 dataSourceName: dataSource.name,
26 runtime: {
27 name: "APPSYNC_JS",
28 runtimeVersion: "1.0.0",
29 },
30 code: readFileSync("./graphql/resolvers/js/get-item.js", "utf-8"),
31 }
32);
33
34// Add a dependency to both resolvers so they are created after the data source
35createItemResolver.addDependency(dataSource);
36getItemResolver.addDependency(dataSource);
ts

In this code snippet, we define two new JS resolvers on our GraphQL API using the `CfnResolver` construct. In each of the definitions, we define whether it’s a `Query` or `Mutation` as well as the `fieldName` required in the request to trigger the resolver. We also link the data source we created earlier to each resolver and provide the compiled JS code we’d like to run when the resolver is triggered.

Finally, at the end of the snippet, we add a new dependency onto each of the resolvers to ensure that they are created after the data source is created to avoid any issues during the deployment process.

Testing

At this point, we’re almost ready to hit deploy and then test our new GraphQL API and see the environment variables in action. But, first, before we can do that we need to add a `CfnOutput` to the bottom of our stack to log out the API URL we need to use as well as the API key we need to provide on each request.

To add this to the stack definition file, add the code below under the code we just added for the resolver definitions.

./lib/*-stack.ts
1// Output our API URL and API key value
2new cdk.CfnOutput(this, "appsync-envs-output", {
3 value: JSON.stringify({
4 url: api.attrGraphQlUrl,
5 apiKey: apiKey.attrApiKey,
6 }),
7});
ts

Then, with that code added we’re ready to deploy our stack by running `npm run deploy` in the terminal and accepting any prompts given to us. You should also notice that in the terminal, the `predeploy` command is run automatically before `deploy` to ensure we deploy the latest code for our JS resolvers.

Once the deployment has finished and you have the API URL and key you need to use. Open a tool like Postman and send a new GraphQL request to your API URL. Make sure to add your API key to the request headers otherwise, you’ll receive a 401 unauthorised response.

If you’re using Postman, you can add the API key by selecting the “Authorization” tab and then selecting the type of “API Key” with the key having a value of `x-api-key` and a value equal to the API key value given to you in the terminal.

At this point, we’re now able to send our queries to the API. Firstly, let’s perform a mutation to create a new item in the database. You can do this, by adding the below code to your query and sending it.

1mutation createItem {
2 createItem {
3 id
4 value
5 }
6}
graphql

You should receive a response like the one below from the API.

1{
2 "data": {
3 "createItem": {
4 "id": "5c6e00fc-bb20-4fa9-a56d-2c7ff7ed792f",
5 "value": "some-secret-value"
6 }
7 }
8}
json

In this response, the `id` will be automatically generated by the resolver and will differ for each item created. But, what we’re primarily interested in here is the `value` property which you can see is using the value of the environment variable we added to our API definition at the start of this tutorial! 🎉

Finally, let’s test everything was stored in the database correctly. So, switch out your mutation for the query below and make sure to add in the `id` from the item you just created.

1query getItem {
2 getItem(id: "YOUR_ITEM_ID") {
3 id
4 value
5 }
6}
graphql

Then, when you run this query, you should get a response back from the API that looks like the one below.

1{
2 "data": {
3 "getItem": {
4 "id": "YOUR_ITEM_ID",
5 "value": "some-secret-value"
6 }
7 }
8}
json

If this is the case then you can see the value we assigned to the environment variable was correctly used in the resolver and stored on the item in the database!

Closing Thoughts

At this point, if you got all the same outputs as shown then you have successfully managed to build a GraphQL API using AWS AppSync via the AWS CDK. And, in that API you have successfully used the new environment variables feature available for AppSync GraphQL APIs, you have also retrieved those environment variables in a JS resolver.

If you would like to read more about environment variables support in AppSync, make sure to check out the AWS documentation. Also, if you’re interested in checking out the full example code for this project, check it out on my CDK tutorials GitHub repository.

Finally, if you’re interested in learning more about making GraphQL APIs using AWS AppSync and the AWS CDK, make sure to check out my other tutorials below.

Thank you for reading.



Content

Latest Blog Posts

Below is my latest blog post and a link to all of my posts.

View All Posts

Content

Latest Video

Here is my YouTube Channel and latest video for your enjoyment.

View All Videos
AWS Bedrock Text and Image Generation Tutorial (AWS SDK)

AWS Bedrock Text and Image Generation Tutorial (AWS SDK)

Contact

Join My Newsletter

Subscribe to my weekly newsletter by filling in the form.

Get my latest content every week and 0 spam!