DEV Community

Mikaeel Khalid for AWS Community Builders

Posted on • Originally published at blog.mikaeels.com on

Building an AWS AppSync Serverless Application using AWS CDK

Image description

In this blog post, we will explore how to build a serverless application using AWS AppSync and AWS Cloud Development Kit (CDK). We will create a simple note-taking application with Create and Read functionality, powered by AWS AppSync, AWS Lambda, and DynamoDB.

Introduction to AWS AppSync:

AWS AppSync is a fully managed service that makes it easy to develop GraphQL APIs by handling the heavy lifting of securely connecting to data sources like DynamoDB, Lambda, and more. It allows you to build real-time and offline-capable applications with a flexible and powerful API.

AWS CDK Overview:

AWS Cloud Development Kit (CDK) is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation. It enables developers to build and manage AWS infrastructure using familiar programming languages like TypeScript, JavaScript, Python, Java, and C#. With CDK, you can easily create, configure, and deploy AWS resources.

Project Overview:

We will create a simple note-taking application with the following features:

  1. A GraphQL API to interact with notes.

  2. A Lambda function to handle the GraphQL resolvers.

  3. A DynamoDB table to store the notes.

Prerequisites:

Before we begin, make sure you have the following prerequisites installed on your machine:

  1. Node.js

  2. AWS CDK

Step 1: Setting Up the Project

Let's start by setting up our project. Open your terminal and follow these steps:

# Create a new directory for your project
mkdir cdk-appsync-serverless-notes-app
cd cdk-appsync-serverless-notes-app

# Initialize a new Node.js project
cdk init --language=typescript

# Create a new directory for Lambda and GraphQL schema
mkdir lambdas graphql

Enter fullscreen mode Exit fullscreen mode

Step 2: Create a new CDK Stack

Create a new CDK stack using the following code:

  1. Import Required AWS CDK Libraries:
import { Duration, Expiration, Stack, StackProps } from 'aws-cdk-lib';
import { GraphqlApi, SchemaFile, AuthorizationType } from 'aws-cdk-lib/aws-appsync';
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { join } from 'path';

Enter fullscreen mode Exit fullscreen mode

In this section, we import the necessary AWS CDK constructs and AWS AppSync, AWS Lambda, and AWS DynamoDB libraries.

  1. Define the AppsyncServerlessStack Class:
export class AppsyncServerlessStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    // ...
  }
}

Enter fullscreen mode Exit fullscreen mode

Here, we define a class named AppsyncServerlessStack, which extends the Stack class from AWS CDK. This class will be used to define our AWS infrastructure stack.

  1. Create the GraphQL API:
const api = new GraphqlApi(this, 'notes-appsync-api', {
  name: 'notes-appsync-api',
  schema: SchemaFile.fromAsset('graphql/schema.graphql'),
  authorizationConfig: {
    defaultAuthorization: {
      authorizationType: AuthorizationType.API_KEY,
      apiKeyConfig: {
        expires: Expiration.after(Duration.days(365)),
      },
    },
  },
  xrayEnabled: true,
});

Enter fullscreen mode Exit fullscreen mode

In this section, we create an instance of GraphqlApi, which represents our AWS AppSync GraphQL API. We provide the API name, and the GraphQL schema file location, and specify that API_KEY authorization is used with an expiration of 365 days. We also enable AWS X-Ray for the API to gain insights into performance and errors.

  1. Create the Lambda Function for Resolvers:
const notesLambda = new NodejsFunction(this, 'notes-lambda', {
  functionName: 'notes-lambda',
  runtime: Runtime.NODEJS_16_X,
  entry: join(__dirname, '../lambdas/notesLambda.ts'),
  memorySize: 1024,
});

Enter fullscreen mode Exit fullscreen mode

In this part, we create a Node.js AWS Lambda function using the NodejsFunction construct. This function will handle the GraphQL resolvers for our API. We specify the function's name, runtime, entry file location, and memory size.

  1. Create a Data Source for the Lambda Function:
const lambdaDataSource = api.addLambdaDataSource('lambda-data-source', notesLambda);

Enter fullscreen mode Exit fullscreen mode

Here, we create a data source for the GraphQL API, which is linked to the Lambda function we created earlier. This data source allows the API to interact with the Lambda function as a data source.

  1. Create Resolvers for GraphQL Queries and Mutations:
lambdaDataSource.createResolver('query-resolver', {
  typeName: 'Query',
  fieldName: 'listNotes',
});

lambdaDataSource.createResolver('mutation-resolver', {
  typeName: 'Mutation',
  fieldName: 'createNote',
});

Enter fullscreen mode Exit fullscreen mode

In this section, we create resolvers for the GraphQL queries and mutations. We specify the type and field names from the GraphQL schema and link them to the corresponding functions in the Lambda function.

  1. Create a DynamoDB Table to Store Notes:
const notesTable = new Table(this, 'notes-api-table', {
  tableName: 'notes-api-table',
  billingMode: BillingMode.PAY_PER_REQUEST,
  partitionKey: {
    name: 'id',
    type: AttributeType.STRING,
  },
});

Enter fullscreen mode Exit fullscreen mode

Here, we create a DynamoDB table to store the notes. We specify the table name, and billing mode (PAY_PER_REQUEST means we pay per request), and define a partition key named 'id' of type STRING.

  1. Grant Lambda Function Access to the DynamoDB Table:
notesTable.grantFullAccess(notesLambda);

Enter fullscreen mode Exit fullscreen mode

This line grants the Lambda function (notesLambda) full access to the DynamoDB table (notesTable). It allows the Lambda function to read and write data to the table.

  1. Add DynamoDB Table Name as an Environment Variable for the Lambda Function:
notesLambda.addEnvironment('NOTES_TABLE', notesTable.tableName);

Enter fullscreen mode Exit fullscreen mode

We add an environment variable to the Lambda function named NOTES_TABLE, and its value is set to the name of the DynamoDB table. This allows the Lambda function to know which table it should interact with.

Here is a complete CDK stack code:

// Import required AWS CDK libraries
import { Duration, Expiration, Stack, StackProps } from 'aws-cdk-lib';
import {
  GraphqlApi,
  SchemaFile,
  AuthorizationType,
} from 'aws-cdk-lib/aws-appsync';
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { join } from 'path';

export class AppsyncServerlessStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // Create the GraphQL API
    const api = new GraphqlApi(this, 'notes-appsync-api', {
      name: 'notes-appsync-api',
      schema: SchemaFile.fromAsset('graphql/schema.graphql'),
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: AuthorizationType.API_KEY,
          apiKeyConfig: {
            expires: Expiration.after(Duration.days(365)),
          },
        },
      },
      xrayEnabled: true,
    });

    // Create the Lambda function for the resolvers
    const notesLambda = new NodejsFunction(this, 'notes-lambda', {
      functionName: 'notes-lambda',
      runtime: Runtime.NODEJS_16_X,
      entry: join(__dirname, '../lambdas/notesLambda.ts'),
      memorySize: 1024,
    });

    // Create a data source for the Lambda function
    const lambdaDataSource = api.addLambdaDataSource(
      'lambda-data-source',
      notesLambda
    );

    // Create resolvers for the GraphQL queries and mutations
    lambdaDataSource.createResolver('query-resolver', {
      typeName: 'Query',
      fieldName: 'listNotes',
    });

    lambdaDataSource.createResolver('mutation-resolver', {
      typeName: 'Mutation',
      fieldName: 'createNote',
    });

    // Create a DynamoDB table to store the notes
    const notesTable = new Table(this, 'notes-api-table', {
      tableName: 'notes-api-table',
      billingMode: BillingMode.PAY_PER_REQUEST,
      partitionKey: {
        name: 'id',
        type: AttributeType.STRING,
      },
    });

    // Grant the Lambda function full access to the DynamoDB table
    notesTable.grantFullAccess(notesLambda);

    // Add the DynamoDB table name as an environment variable for the Lambda function
    notesLambda.addEnvironment('NOTES_TABLE', notesTable.tableName);
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Define the GraphQL Schema

In this step, we will define the GraphQL schema for our note-taking application. Create a file named schema.graphql inside the graphql directory with the following content:

type Note {
  id: ID!
  name: String!
  completed: Boolean!
}

input NoteInput {
  id: ID!
  name: String!
  completed: Boolean!
}

type Query {
  listNotes: [Note]
}

type Mutation {
  createNote(note: NoteInput!): Note
}

type Subscription {
  onCreateNote: Note @aws_subscribe(mutations: ["createNote"])
}

Enter fullscreen mode Exit fullscreen mode

Step 4: Implement the Lambda Resolvers

Next, let's implement the Lambda function that will handle the GraphQL resolvers. Create a file named notesLambda.ts inside the lambdas directory with the following code:

// Import the required types
import { createNote, listNotes } from './notesOperations';
import { Note } from './types/Note';

// Define the AppSyncEvent type
type AppSyncEvent = {
  info: {
    fieldName: string;
  };
  arguments: {
    noteId: string;
    note: Note;
  };
};

// Lambda function handler
exports.handler = async (event: AppSyncEvent) => {
  switch (event.info.fieldName) {
    case 'createNote':
      return await createNote(event.arguments.note);

    case 'listNotes':
      return await listNotes();

    default:
      return null;
  }
};

Enter fullscreen mode Exit fullscreen mode

Step 5: Implement the Lambda Operations

Now, let's implement the actual operations for creating and listing notes in DynamoDB. Create a file named notesOperations.ts inside the lambdas directory with the following code:

// Import the AWS SDK and the Note type
import { DynamoDB } from 'aws-sdk';
import { Note } from '../types/Note';

// Create a DynamoDB DocumentClient
const docClient = new DynamoDB.DocumentClient();

// Function to create a new note
export const createNote = async (note: Note) => {
  const params = {
    TableName: process.env.NOTES_TABLE as string,
    Item: note,
  };

  try {
    const data = await docClient.put(params).promise();
    console.log('data', data);
    return note;
  } catch (error) {
    console.log('dynamodb err: ', error);
    return null;
  }
};

// Function to list all notes
export const listNotes = async () => {
  const params = {
    TableName: process.env.NOTES_TABLE as string,
  };

  try {
    const data = await docClient.scan(params).promise();
    return data.Items;
  } catch (error) {
    console.log('dynamodb err: ', error);
    return null;
  }
};

Enter fullscreen mode Exit fullscreen mode

Step 6: Deploy the Application

Now that we have implemented the GraphQL API, Lambda resolvers, and DynamoDB operations, it's time to deploy our application. Make sure you have installed and configured the AWS CLI on your machine.

Run the following commands to deploy the CDK stack:

# Install dependencies
npm install

# Booststrap the CDK Env (if not already)
cdk bootstrap

# Synthesize the CDK stack
cdk synth

# Deploy the CDK stack
cdk deploy

Enter fullscreen mode Exit fullscreen mode

Once the deployment is complete, the AWS CloudFormation will create the necessary resources, including the GraphQL API, Lambda function, and DynamoDB table.

Conclusion:

Congratulations! You have successfully built an AWS AppSync serverless application using AWS CDK. You now have a GraphQL API to interact with your note-taking application, powered by AWS Lambda and DynamoDB. This is just the beginning, and you can extend the application with additional features like authentication, real-time updates using subscriptions, and more.

I hope you found this blog post helpful and that it inspires you to explore further and build more sophisticated serverless applications using AWS AppSync and AWS CDK. Happy coding!

GitHub Repo:

https://github.com/mikaeelkhalid/aws-cdk-appsync-serverless-boilerplate

Top comments (0)