Building Full Stack Serverless Application With Amplify, Flutter, GraphQL, AWS CDK, and Typescript(PART 2)

Building Full Stack Serverless Application With Amplify, Flutter, GraphQL, AWS CDK, and Typescript(PART 2)

notes.jpg

Hi there, in PART 1 of this series, we introduced basic concepts of the underlying technologies we need to build our application.
Continuing from where we left off,

Prerequisites

AWS Account
Configured AWS CDK on your computer.

Getting Started.

To get started, we'll be using the CDK CLI to initialize a new project. To do so, create a new empty folder and initialize a new CDK project in TypeScript:

mkdir notes-cdk-app &&  cd notes-cdk-app
cdk init --language=typescript

Once the command has been completed successfully, you should see a bunch of files and folders would be created. The root stack resides in the lib folder in a file called notes-cdk-app-stack.ts
To create this API, we need a couple of AWS resources. In CDK, they are called constructs.
Constructs are cloud components and encapsulate everything AWS cloudformation needs to create that component.
They are the basic building blocks of CDK apps.
For this project, we will need these constructs

  • Amazon Cognito (authentication)
  • Amazon DynamoDB (As a database to store all application data)
  • AWS AppSync (GraphQL API, real-time)
  • AWS Lambda (Lambda Functions) Let's go ahead and install them now
npm install @aws-cdk/aws-appsync @aws-cdk/aws-lambda @aws-cdk/aws-dynamodb @aws-cdk/aws-cognito

Running a build

This application is created in typescript and would ultimately be converted to javascript before deploying. Therefore, we need to create a build that would do the conversion.
There are 2 ways to accomplish this.

  • Using Watch mode npm run watch that'll automatically compile to javascript upon saving the file
  • Using manual build by running npm run build each time you want to compile.

Creating the API

Navigate to the llib/notes-cdk-app-stack.ts and open it up. That's the root of the CDK app and it's where we would be writing our app.
Let's go ahead and import all our CDK modules

import * as cdk from "@aws-cdk/core";
import * as appsync from "@aws-cdk/aws-appsync";
import * as ddb from "@aws-cdk/aws-dynamodb";
import * as lambda from "@aws-cdk/aws-lambda";
import * as cognito from "@aws-cdk/aws-cognito";

Creating Cognito User Pools and User Pool Clients

Our API provides both public and private access to its resources.
For private access, we will be using Cognito to give users the possibility to create accounts in order to access the API.
Within this block of code,

export class NotesCdkAppStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
}
}

Add the following code

    const userPool = new cognito.UserPool(this, "cdk-notes-user-pool", {
      selfSignUpEnabled: true,
      accountRecovery: cognito.AccountRecovery.PHONE_AND_EMAIL,
      userVerification: {
        emailStyle: cognito.VerificationEmailStyle.CODE,
      },
      autoVerify: {
        email: true,
      },
      standardAttributes: {
        email: {
          required: true,
          mutable: true,
        },
      },
    });

    const userPoolClient = new cognito.UserPoolClient(this, "UserPoolClient", {
      userPool,
    });

    new cdk.CfnOutput(this, "UserPoolId", {
      value: userPool.userPoolId,
    });

    new cdk.CfnOutput(this, "UserPoolClientId", {
      value: userPoolClient.userPoolClientId,
    });

We've defined a user pool with the following configuration.

  • selfSignUpEnabled:Allow users to be able to sign up on their own
  • accountRecovery : In case a user forgot their passwords, they should be able to recover it using PHONE or Email
  • userVerification{emailStyle}: Sends user an email with a code to verify their account.
  • autoVerify: We would be creating some users(test only)on Cognito in the AWS console and we want them to continue using the credentials without verifying their emails.
  • standardAttributes : All users in a user pool have a set of standard attributes such as given names, address, email, birthdate, etc. Here, we set the standard attribute to email and make it mutable and required.

After creating the user pool, we use it to initialize a user pool client and then output both the userPoolId and userPoolClientId using cdk.CfnOutput.

Creating AppSync API

The next step is to instantiate an appsync api, with the following configuration:
Add the following code below the User Pool Client definition in lib/notes-cdk-app-stack.ts:

 const api = new appsync.GraphqlApi(this, "Api", {
      name: "cdk-notes-appsync-api",
      schema: appsync.Schema.fromAsset("graphql/schema.graphql"),
      authorizationConfig: {
        defaultAuthorization: {
          authorizationType: appsync.AuthorizationType.API_KEY,
          apiKeyConfig: {

            expires: cdk.Expiration.after(cdk.Duration.days(365)),
          },
        },
        additionalAuthorizationModes: [
          {
            authorizationType: appsync.AuthorizationType.USER_POOL,
            userPoolConfig: {
              userPool,
            },
          },
        ],
      },
      xrayEnabled: true,
    });
  • name: Defines the name of the AppSync API
  • schema: Specifies the location of the GraphQL schema
  • authorizationConfig: This allows you to define the default authorization mode its configuration, as well as (optional) additional authorization modes
  • additionalAuthorizationModes: This allows you to define more authorization modes for your API.This is where we use the user pool we created above -xrayEnabled: Enables xray debugging.

Let's define a graphQL schema. It's located in a file called schema.graphql in a folder called graphql.

type Note @aws_api_key @aws_cognito_user_pools {
  id: ID!
  title: String!
  description: String!
  color: String!
  createdOn: AWSTimestamp
}

input NoteInput {
  id: ID!
  title: String!
  description: String!
  color: String!
  createdOn: AWSTimestamp
}

input UpdateNoteInput {
  id: ID!
  title: String!
  description: String!
  color: String!
  updatedOn: AWSTimestamp
}

type Query {
  getNoteById(noteId: String!): Note @aws_api_key @aws_cognito_user_pools
  listNotes: [Note] @aws_api_key @aws_cognito_user_pools
}

type Mutation {
  createNote(note: NoteInput!): Note @aws_cognito_user_pools
  updateNote(note: UpdateNoteInput!): Note @aws_cognito_user_pools
  deleteNote(noteId: String!): String @aws_cognito_user_pools
}
type Subscription {
  onCreateNote: Note @aws_subscribe(mutations: ["createNote"])
  onDeleteNote: String @aws_subscribe(mutations: ["deleteNote"])
  onUpdateNote: Note @aws_subscribe(mutations: ["updateNote"])
}

It contains 2 queries, 3 mutations, and 3 subscriptions.
It also has appsync directives to provide public and private access to each endpoint.
For example, the createNote endpoint can only be accessed by signed In users, while the listNotes endpoint can be accessed by both signedIn and non SignedIn users.

Adding a Lambda Source

Now that we’ve created the API, we need a way to connect the GraphQL operations (createNote, updateNote, listNotes, etc..) to a data source. We will be doing this by mapping the operations into a Lambda function that will be interacting with a DynamoDB table.

To build out this functionality, we’ll need to create the Lambda function and then add it as a data source to the AppSync API.
Add the following code below the API definition in lib/notes-cdk-app-stack.ts:

const notesLambda = new lambda.Function(this, "AppSyncNotesHandler", {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: "main.handler",
      code: lambda.Code.fromAsset("lambda-fns"),
      memorySize: 1024,
    });

 // Set the new Lambda function as a data source for the AppSync API
    const lambdaDs = api.addLambdaDataSource("lambdaDatasource", notesLambda);

Attaching the GraphQL resolvers

Now that the Lambda DataSource has been created, we need to enable the Resolvers for the GraphQL operations to interact with the data source.
To do so, we can add the following code below the Lambda data source definition:

lambdaDs.createResolver({
      typeName: "Query",
      fieldName: "getNoteById",
    });

    lambdaDs.createResolver({
      typeName: "Query",
      fieldName: "listNotes",
    });

    lambdaDs.createResolver({
      typeName: "Mutation",
      fieldName: "createNote",
    });

    lambdaDs.createResolver({
      typeName: "Mutation",
      fieldName: "deleteNote",
    });

    lambdaDs.createResolver({
      typeName: "Mutation",
      fieldName: "updateNote",
    });

That's all for this post.
In the next post, we'll look into adding our DynamoDB Table.
If you've read this far, I appreciate you.
If you loved it, there's a high chance, someone in your clique would love it too. Show a brother some love, by spreading the word, leaving a like or comment.
See you in the next article
Happy Coding ✌🏿