Add the API
In the previous part of this tutorial, the authentication was added as a service in Amplify, and also the application can now verify if the user is authenticated with the user pools or through an API key to make requests. This step will show how to add the GraphQL API as a service in Amplify and configure it to work with the React application.
The application needs to have the following functionalities:
- Client-side:
- List available T-Shirt models
- Create new orders
- Order information
- Client information
- List orders
- See order status
- Create new orders
- List available T-Shirt models
- Owner side
- CRUD orders
- CRUD T-shirt models
According to the list above, the app needs four tables: Orders, Clients, Products, and Delivery. To start creating the APIs run:
amplify add api
This is the process we used for this tutorial:
Once it is finished, VS Code is going to open the GraphQL template that Amplify created. Here is where you define the tables that we mentioned above. This is what was created for this tutorial:
type Client @model @auth(rules: [{ allow: owner, provider: userPools }]) {
id: ID!
user: String!
name: String!
lastname: String!
city: String!
state: String!
country: String!
zip: String!
phone: String!
email: AWSEmail!
deliveryAddress: [Delivery!] @hasMany
orders: [Order] @hasMany
}
type Delivery @model @auth(rules: [{ allow: owner, provider: userPools }]) {
id: ID!
city: String!
state: String!
country: String!
zip: String!
phone: String!
address: String!
details: String
order: Order @belongsTo
client: Client @belongsTo
}
type Product @model @auth(rules: [
{ allow: public, operations: [read], provider: iam},
{ allow: owner, provider: userPools }
]) {
id: ID!
name: String!
price: Float!
weight: Float
options: [ProductOptions!]!
thumbnail: AWSURL!
images: [AWSURL!]!
description: String!
avilable: Boolean!
unlimited: Boolean!
extraDetails: String
order: [Order!] @manyToMany(relationName: "ProductOrders")
}
type ProductOptions {
name: String!
thumbnail: String!
colorCode: String!
stock: Int!
}
type Order @model @auth(rules: [{ allow: owner, provider: userPools }]) {
id: ID!
title: String!
date: AWSDateTime!
total: Float!
orderDetails: [OrderDetail!]!
client: Client @belongsTo
delivery: Delivery @hasOne
status: OrderStatus!
paymentStatus:PaymentStatus!
paymentType: PaymentType!
products: [Product!] @manyToMany(relationName: "ProductOrders")
}
type OrderDetail {
productID: String!
productName: String!
productColor: String!
productThumbnail: String!
quantity: Int!
total: Float!
}
enum OrderStatus {
received
procesing
delivering
delivered
canceled
}
enum PaymentStatus {
procesing
acepted
rejected
pending
}
enum PaymentType {
cash
bankWire
creditCard
paypal
}
To use this, deploy the created API to your AWS account with:
amplify push
As it is creating a new API, Amplify will ask if you want to generate the code for the newly created API. This part is really important since it generates the infrastructure as code (IaC), and also types for all the possible operations for the API that can be used in the front end.
Now the API is ready to be used in the application. Amplify will display the endpoint and the API KEY ready to be used:
Use the API in the React frontend
In order to use the GraphQL API pushed to Amplify, AWS provides all the necessary tools through the aws-amplify dependency, the aws-exports file, and all the types that were created when the API was generated by the CLI.
Below you will find an example of how to create the products with the GraphQL API.
These are the imports required and what they do:
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
import awsExports from './aws-exports';
import * as AwsUI from '@awsui/components-react';
import * as UiReact from '@aws-amplify/ui-react';
import { useForm } from 'react-hook-form';
import { createProduct } from './graphql/mutations';
import { CreateProductInput } from './API';
import { GraphQLResult } from '@aws-amplify/api-graphql';
import { Amplify, API, graphqlOperation } from 'aws-amplify';
import { Authenticator, View } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
createProduct: This is the GraphQL API mutation used to accept an object which contains the new product info and create it.
CreateProductInput: This is the type that you can use for the form validation. It contains all the fields that a product should have. So it’s pretty easy to use a form validator and feed it with this type. (Generated by the AWS CLI)
Amplify: Helps to load the aws-exports file that contains locally all that has been generated from AWS.
API: This class facilitates all the operations that can be performed by the API.
graphqlOperation: Generates the parameters needed to perform a GraphQL request.
To create a new product you can create a form and a function that calls the API for the creation. Here you can see an example of you to do it:
const addProduct = (data: CreateProductInput) => {
setLoading(true);
const createProductRequest: Promise<GraphQLResult<any>> = API.graphql({
...graphqlOperation(createProduct, { input: data }),
authMode: 'AMAZON_COGNITO_USER_POOLS',
});
createProductRequest
.then(() => {
alert('Product created!');
})
.catch(() => alert('There was an error creating the product'))
.finally(() => setLoading(false));
};
Here on product the data to be created is prepared, then on createProduct the API mutation promise is prepared. You can see that authMode was added, this is because if you are using multiple authentication methods you have to specify which method to use in the request.
Finally, the promise is handled if it succeeds, the user is redirected to the list of products and a message is shown. If it fails an alert is presented.
Here you can see the full example using react-hook-form and it's really interesting how the generated types can be used to create the form and provide immediate validation:
[imports ...]
Amplify.configure(awsExports);
function App() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<CreateProductInput>();
const { signOut } = useAuthenticator((context) => [context.user]);
const [isLoading, setLoading] = useState(false);
const addProduct = (data: CreateProductInput) => {
setLoading(true);
const createProductRequest: Promise<GraphQLResult<any>> = API.graphql({
...graphqlOperation(createProduct, { input: data }),
authMode: 'AMAZON_COGNITO_USER_POOLS',
});
createProductRequest
.then(() => {
alert('Product created!');
})
.catch(() => alert('There was an error creating the product'))
.finally(() => setLoading(false));
};
return (
<AppLayout
navigation={
<>
<SpaceBetween direction="vertical" size="l">
<h1>T-shirts</h1>
<Button onClick={signOut} variant="primary">
Sign Out
</Button>
</SpaceBetween>
</>
}
content={
<Form
actions={
<Button
loading={isLoading}
formAction="submit"
onClick={() => handleSubmit(addProduct)()}
variant="primary"
>
Create Product
</Button>
}
>
<SpaceBetween direction="vertical" size="l">
<Container
header={
<Header variant="h3">
Complete the following form and add the variants:
</Header>
}
>
<SpaceBetween direction="vertical" size="l">
<TextField
label="Name"
placeholder="T-shirt"
{...register('name', { required: true })}
hasError={errors.name && true}
errorMessage="Product should be named"
/>
<TextField
label="Price"
{...register('price', { required: true })}
hasError={errors.price && true}
errorMessage="Add a price"
/>
…
<TextField
label="Stock"
{...register('options.0.stock', {
required: true,
min: 0,
max: 100,
})}
hasError={errors.options && true}
errorMessage="Specify the stock"
/>
</SpaceBetween>
</Container>
</SpaceBetween>
</Form>
}
/>
);
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Authenticator.Provider>
<Authenticator>
<App />
</Authenticator>
</Authenticator.Provider>
</React.StrictMode>
);
API mutations:
You can create an abstraction of all the API mutations to reuse them, heare are some examples:
Create:
export const addElement = <T>(data: T, createMutation: string) => {
const createElement: Promise<GraphQLResult<any>> = API.graphql({
...graphqlOperation(createMutation, { input: data }),
authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
});
return createElement;
};
Update:
export const updateElement = <T>(
data: T,
elementId: string,
updateMutation: string
) => {
const element: T = {
...data,
id: elementId,
};
const updatedElement: Promise<GraphQLResult<any>> = API.graphql({
...graphqlOperation(updateMutation, { input: element }),
authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
});
return updatedElement;
};
Delete:
export const deleteElement = (elementId: string, deleteMutation: string) => {
const deleteElementResult: Promise<GraphQLResult<any>> = API.graphql({
query: deleteMutation,
variables: { input: { id: elementId } },
authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
});
return deleteElementResult;
};
Get element:
export const getElement = (elementId: string, getQuery: string) => {
const getElementResult: Promise<GraphQLResult<any>> = API.graphql({
...graphqlOperation(getQuery, { id: elementId }),
authMode: GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS,
});
return getElementResult;
};
To see the full demo application of how to handle all the different CRUD operations with the Amplify API follow this link to check the repo.
Analyze the control access to the API according to user permissions
It’s worth creating this section to analyze the schema.graphql file and how it is handling the permissions.
Amplify has an @auth directive to configure the authorization rules for public, sign-in user, per-user, and per-user group data access. To apply this to the GraphQL schema add the @auth directive including the rules that the model needs, as follows:
amplify\backend\api\tshirts\schema.graphql
type Client @model @auth(rules: [{ allow: owner, provider: userPools }]) {
id: ID!
user: String!
name: String!
…
}
allow: owner
defines that only the user that owns this model can have access to it. provider: userPools
defines the provider method to be userPools, which means that the user has to be authenticated to access this model.
amplify\backend\api\tshirts\schema.graphql
type Product @model @auth(rules: [
{ allow: public, operations: [read], provider: iam},
{ allow: owner, provider: userPools }
]) {
id: ID!
name: String!
price: Float!
…
}
For the product, the rule needs to be different. The product catalog should be public, but only for reading. The rules can be defined by the type of operation so allow: public
allows public access, operations: [read]
the type of operation for this rule, and provider: iam
allows unauthenticated users to access the information.
amplify\backend\api\tshirts\schema.graphql
input AMPLIFY { globalAuthRule: AuthRule = { allow: public } }
If you are starting and just want to test your application, Amplify defines this rule by default. So every model can be publicly accessible to everyone.
To check more options, go to the AWS authorization rules docs here.
Demo source code:
Check the full source code of this part of the tutorial here: https://github.com/natar10/tshirts-amplify/tree/part-3-add-api
Next steps:
- Part 4: (coming soon) Hosting and CI/CD
So far, this tutorial has explored how to scaffold a React application, install and configure amplify, add authentication, add the API and how to use it. Now let’s assume that the application is ready and needs to be deployed somewhere. Amplify can provide an integrated CI/CD workflow with Github and storage to deploy, you can check how to do this in Part 4 of this tutorial