Front-End Web & Mobile

Developing and testing GraphQL APIs, Storage and Functions with Amplify Framework Local Mocking features

This article was written by Ed Lima, Sr. Solutions Architect, AWS and Sean Grove, OneGraph

In fullstack application development, iteration is king. At AWS, we’re constantly identifying steps in the process of shipping product that slow iteration, or sap developer productivity and happiness, and work to shorten it. To that end, we’ve provided cloud APIs, serverless functions, databases, and storage capabilities so that the final steps of deploying, scaling, and monitoring applications are as instantaneous as possible.

Today, we’re taking another step further in shortening feedback cycles by addressing a critical stage in the application cycle: local development.

Working closely with developers, we’ve seen the process of delivering new product features to production:

  1. Prototyping changes locally
  2. Committing and pushing to the cloud resources
  3. Mocking/testing/debugging the updates
  4. Returning to step 1 if there are any fixes to incorporate

In some cases, this can be an incredibly tight loop, executed dozens or hundreds of times by a developer, before new features are ready to ship. It can be a tedious process, and tedious processes make unhappy developers.

AWS AppSync gives developers easy, and convenient access to exactly the right data they need at a global scale via its flexible GraphQL APIs. These APIs, among other data sources, can be backed by Amazon DynamoDB for a scalable key-value and document database that delivers single-digit millisecond performance at any scale. Applications can also use Amazon Simple Storage Service (S3) for an object storage service that offers industry-leading scalability, data availability, security, and performance. On top of it, developers can run their code without provisioning or managing servers with AWS Lambda. All of these services live in the cloud, which is great for production – highly available, fault tolerant, scaling to meet any demand, running in multiple availability zones in different AWS regions around the planet.

In order to optimize and streamline the feedback loop between local and cloud resources earlier in the development process, we talked to many customers to understand their requirements for local development:

  • NoSQL data access via a robust GraphQL API
  • Serverless functions triggered for customized business logic from any GraphQL type or operation
  • Developer tooling, including a GraphiQL IDE fully pre-integrated with open-source plugins such as those from OneGraph, customized for your AppSync API
  • Simulated object storage
  • Instantaneous feedback on changes
  • Debugging GraphQL resolver mapping templates written in Velocity Template Language (VTL)
  • Ability to use custom directives and code generation with the GraphQL Transformer
  • Ability to mock JWT tokens from Amazon Cognito User Pools to test authorization rules locally
  • Work with web and mobile platforms (iOS and Android)
  • And, the ability to work offline

With the above customer requirements in mind we’re happy to launch the new Local Mocking and Testing features in the Amplify Framework.

As a developer using Amplify, you’ll immediately see the changes you make locally to your application, speeding up your development process and removing interruptions to your workflow. No waiting for cloud services to be deployed – just develop, test, debug, model your queries, and generate code locally until you’re happy with your product, then deploy your changes to the scalable, highly available backend services in the cloud as you’ve always done.

Getting Started

To get started, install the latest version of the Amplify CLI by following these steps, and follow along with our example below. Use a boilerplate React app created with create-react-app and initialize an Amplify project in the app folder with the default options by executing the amplify init command. Note, the local mocking and testing features in the Amplify CLI will also work with iOS and Android apps.

Next, we add a GraphQL API using the command amplify add api with API Key authorization and the sample schema for single object with fields (Todo):

When defining a GraphQL schema you can use directives from the GraphQL Transformer in local mocking as well as local code generation from the schema for GraphQL operations. The following directives are currently supported in the local mocking environment:

  • @model
  • @auth
  • @key
  • @connection
  • @versioned
  • @function

The sample GraphQL schema generated by the Amplify CLI has a single “Todo” type defined with @model which means the GraphQL Transformer will automatically create a GraphQL API with an extended schema containing queries, mutations, and subscriptions with built-in CRUDL logic to access a DynamoDB table, also automatically deployed. It basically creates a fully-fledged API backend in seconds:

type Todo @model {
  id: ID!
  name: String!
  description: String
}

At this point, your API is ready for some local development! Fire up your local AppSync and DynamoDB resources by executing either the command  amplify mock to test all supported local resources or amplify mock api to test specifically the GraphQL API and watch as a local mock endpoint starts up. Code will be automatically generated and validated for queries, mutations, subscriptions and a local AppSync mock endpoint will start up:

Collaborating with the Open Source community is always special, it has allowed us to improve and better understand the use cases that customers want to tackle with local mocking and testing. In order to move fast and ensure that we were releasing a valuable feature, we worked for several months with a few community members. We want to give a special thanks to Conduit Ventures for creating the AWS-Utils package, as well as allowing us to fork it for this project and integrate with the Amplify new local mocking environment.

Prototyping API calls with an enhanced local GraphiQL IDE

The mock endpoint runs on localhost and simulates an AWS AppSync API connected to a DynamoDB table (defined at the GraphQL schema with the @model directive), all implemented locally on your developer machine.

We also ship tools to explore and interact with your GraphQL API locally. In particular, the terminal will print out a link to an instance of the GraphiQL IDE, where you can introspect the schema types, lookup documentation on any field or type, test API calls, and prototype your queries and mutations:

We’ve enhanced the stock GraphiQL experience with an open-source plugin that OneGraph have created to make your developer experience even nicer. In the Amplify GraphiQL Explorer, you’ll notice an UI generated for your specific GraphQL API that allows to quickly and easily explore, build GraphQL queries, mutations, or even subscriptions by simply navigating checkboxes. You can create, delete, update, read, or list data from your local DynamoDB tables in seconds.

With this new tooling, you can go from exploring your new GraphQL APIs locally to a full running application in a few minutes. Amplify is leveraging the power of open source to integrate the new local mocking environment with tools such as AWS-Utils and the GraphiQL Explorer to streamline the development experience and tighten the iteration cycle even further. If you’re interested in learning more about how and why the explorer was built, check out OneGraph’s blog on how they on-board users who are new to GraphQL.

What if you need to test and prototype real-time subscriptions? They also work seamlessly in the local environment. While amplify mock api is running, open another terminal window and execute yarn add aws-amplify to install some client dependencies then run yarn start.  In order to test, paste the code bellow to the src/App.js file in the React project, replacing the existing boilerplate code generated by the create-react-app command:

import React, { useEffect, useReducer } from "react";
import Amplify from "@aws-amplify/core";
import { API, graphqlOperation } from "aws-amplify";
import { createTodo } from "./graphql/mutations";
import { listTodos } from "./graphql/queries";
import { onCreateTodo, onUpdateTodo } from "./graphql/subscriptions";

import config from "./aws-exports";
Amplify.configure(config); // Configure Amplify

const initialState = { todos: [] };
const reducer = (state, action) => {
  switch (action.type) {
    case "QUERY":
      return { ...state, todos: action.todos };
    case "SUBSCRIPTION":
      return { ...state, todos: [...state.todos, action.todo] };
    default:
      return state;
  }
};

async function createNewTodo() {
  const todo = { name: "Use AppSync", description: "Realtime and Offline" };
  await API.graphql(graphqlOperation(createTodo, { input: todo }));
}
function App() {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    getData();
    const subscription = API.graphql(graphqlOperation(onCreateTodo)).subscribe({
      next: eventData => {
        const todo = eventData.value.data.onCreateTodo;
        dispatch({ type: "SUBSCRIPTION", todo });
      }
    });
    return () => {
      subscription.unsubscribe();
    };
  }, []);

  async function getData() {
    const todoData = await API.graphql(graphqlOperation(listTodos));
    dispatch({ type: "QUERY", todos: todoData.data.listTodos.items });
  }

  return (
    <div>
      <div className="App">
        <button onClick={createNewTodo}>Add Todo</button>
      </div>
      <div>
        {state.todos.map((todo, i) => (
          <p key={todo.id}>
            {todo.name} : {todo.description}
          </p>
        ))}
      </div>
    </div>
  );
}
export default App;

Open two browser windows, one with the local GraphiQL instance and another one with the React App. As you can see in the following animation, you’ll be able to create items, see the mutations automatically triggering subscriptions and displaying the changes in the web app with no need to reload the browser:

 

If you want to access your NoSQL local data directly, as DynamoDB Local uses SQLite internally you can also access the data in the tables by using your IDE extension of choice:

Seamless transition between local and cloud environments

In the screenshot above you’ll notice the GraphQL API is in a “Create” state in the terminal section at the bottom, which means the backend resources are not deployed to the cloud yet. If we check the local “aws_exports.js” file generated by Amplify, which contains the identifiers of the resources created in different categories, you’ll notice the API endpoint is accessed locally and we’re using a fake API Key to authorize calls:

const awsmobile = {
    "aws_project_region": "us-east-1",
    "aws_appsync_graphqlEndpoint": "http://localhost:20002/graphql",
    "aws_appsync_region": "us-east-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "da2-fakeApiId123456"
};

export default awsmobile;

What about testing more refined authentication requirements? You can still authenticate against a Cognito User Pool. The local testing server will honor the JWT tokens generated by Amazon Cognito and the rules defined by the @auth directive in your GraphQL schema. However, as Cognito is not running locally, you need to execute the command amplify push first to create the user pool and easily test users access with, for instance, the Amplify withAuthenticator higher order component on React. After that you can move back to the local environment with the command amplify mock api and authenticate calls with the generated JWT tokens. If you want to test directly from GraphiQL, after your API is configured to use Cognito, the Amplify GraphiQL Explorer provides a way to mock and change the username, groups, and email for a user and generate a local JWT token just by clicking the “Auth” button. The mocked values are used by the GraphQL Transformer @auth directive and any access rules:

After pushing and deploying the changes to the cloud with amplify push, the “aws_exports.js” file will be updated accordingly to point to the appropriate resources:

const awsmobile = {
    "aws_project_region": "us-east-1",
    "aws_appsync_graphqlEndpoint": "https://eriicnzxxxxxxxxxxxxx.appsync-api.us-east-1.amazonaws.com/graphql",
    "aws_appsync_region": "us-east-1",
    "aws_appsync_authenticationType": "API_KEY",
    "aws_appsync_apiKey": "da2-gttjhle72nf3pbfzfil2jy54ne"
};

export default awsmobile;

You can easily move back and forth between local and cloud environments as the identifiers in the exports file are updated automatically.

Local Debugging and Customizing VTL Resolvers

The local mocking environment also allows to easily customize and debug AppSync resolvers. You can edit VTL templates locally and check if they contain errors, including the line numbers causing problems, before pushing to AppSync. In order to do so, with the local API running, navigate to the folder amplify/backend/api/<your API name>/resolvers. You will see a list of resolver templates that the GraphQL Transformer automatically generated. You can modify any of them and, after saving changes, they are immediately loaded into the locally running API service with a message Mapping template change detected. Reloading. If you inject an error, for instance adding an extra curly brace, you will see a meaningful description of the problem and the line where the error was detected as shown below:

In case you stop the mock endpoint, for instance to push your changes to the cloud, all of the templates in the amplify/backend/api/<your API name>/resolvers folder will be removed except for any that you modified. When you subsequently push to the cloud these local changes will be automatically merged with your AppSync API.

As you are developing your app, you can always update the GraphQL schema located at amplify/backend/api/<your API name>/schema.graphql. You can add additional types and any of the supported GraphQL Transform directives then save your changes while the local server is still running. Any updates to the schema will be automatically detected and validated, then immediately hot reloaded into the local API. Whenever you’re happy with the backend, pushing and deploying the changes to the cloud is just one CLI command away.

Integrating Lambda Functions

Today you can already create and invoke Lambda functions written in Node.js locally with the Amplify CLI. Now how can you go even further and integrate lambda functions with GraphQL APIs in the new local mocking environment? It’s very easy to test customized business logic implemented with Lambda in your local API. Let’s start by creating a lambda function for your Amplify project with the command amplify add function to create a function called “factOfTheDay” as follows:

The function calls an external API to retrieve a fact related to the current date. Here’s the code:

const axios = require("axios");
const moment = require("moment");

exports.handler = function(event, _, callback) {
  let apiUrl = `http://numbersapi.com/`;
  let day = moment().format("D");
  let month = moment().format("M");
  let factOfTheDay = apiUrl + month + "/" + day;

  axios
    .get(factOfTheDay)
    .then(response => callback(null, response.data))
    .catch(err => callback(err));
};

Since the function above uses both the axios and moment libraries, we need to install them in the function folder amplify/backend/function/factOfTheDay/src by executing either npm install axios moment or yarn add axios moment. We can also test the function locally with the command amplify mock function factOfTheDay:

In our API we’ll add a field to the “Todo” type so every time we read or create records the Lambda function will be invoked to retrieve the facts of the current day. In order to do that we’ll take advantage of the GraphQL Transformer @function directive and point it to our lambda function by editing the file amplify/backend/api/localdev/schema.graphql:

type Todo @model {
  id: ID!
  name: String!
  description: String
  factOfTheDay: String @function(name: "factOfTheDay-${env}")
}

In order to test, we execute amplify mock to test locally all the mocked categories (in this case, API and Function) and access the local instance of the GraphiQL IDE in the browser:

As you can see, the GraphQL query is successfully invoking the local lambda function as well as retrieving data from the local DynamoDB table with a single call. In order to commit the changes and create the lambda function in the cloud, it’s just a matter of executing amplify push.

Integrating S3 storage

Most apps need access to some sort of content such as audio, video, images, PDFs and S3 is the best way to store these assets. How can we easily bring S3 to our local development environment?

First, let’s add storage to our amplify project with amplify add storage. If you have not previously added the “Auth” category in your project, the “Storage” category will also ask you to set this up and it is OK to do so. While this doesn’t impact local mocking as there are no authorization checks at this time for the Storage category, you must configure it first for cloud deployment to make sure the S3 bucket is secured according to your application requirements:

To start testing, execute amplify mock. Alternatively, you can run amplify mock storage to only mock the Storage category. If you have not pushed Auth resources to the cloud, you’ll need to do so by executing amplify auth push to create/update the Cognito resources as they’ll be needed to secure access to the actual S3 bucket.

You can use any of the storage operations provided by the Amplify library in your application code such as put, get, remove or list as well as use UI components to sign-up/sign-in users and interact with the local content. Files will be saved to your local Amplify project folder under amplify/mock-data/S3. When ready, execute amplify push to create the S3 bucket in the cloud.

Conclusion

With the new local mocking environment, we want to deliver a great experience to developers using the Amplify Framework. Now you can quickly spin up local resources, test, prototype, debug and generate code with open source tools, work on the front-end and create your fullstack serverless application in no time. On top of that, after you’re done and happy with your local development results, you can commit the code to GitHub and link your repository to the AWS Amplify Console which will provide a built-in CI/CD workflow. The console detects changes to the repository and automatically triggers builds to create your Amplify project backend cloud resources in multiple environments as well as publish your front-end web application to a content delivery network. Fullstack local development, testing, debugging, CI/CD, code builds and web publishing made much easier and faster for developers.

It’s just Day 1 for local development, mocking and testing on Amplify, what else would you like to see in our local mocking environment? Let us know if you have any ideas, feel free to create a feature request in our GitHub repository. Our team constantly monitors the repository and we’re always listening to your requests. Go build (now locally in your laptop)!