Getting started with AWS app sync with Next.js app router
aws documentation is really hard to understand. i can't seem to find a starting point anywhere. no way to search for solutions, no tutorials that don't take me back to some sort of "hello world" lambda example.
— Aditya (@adityagodse381) April 9, 2024
Getting started
Initially, I had a hard time, figuring my way around the AWS tool and services. The documentation was really hard for me to understand. I had to take really deep to find a solution for a problem. I would search through the AWS community forums, stack overflow, reddit, median Searching for solution. I will create a series of articles with basic configuration and set ups of AWS tools and services. I am aware, Current come well soon, be outdated, so this series will be for next js version 14
with app router.
Before getting started, here is the official developer guide for AppSync by AWS
AppSync
Graph QL is an amazing technology. I am amazed by the story behind it. Checkout this documentary.
AWS AppSync enables developers to connect their applications and services to data and events with secure, serverless and high-performing GraphQL and Pub/Sub APIs. You can do the following with AWS AppSync:
- Access data from one or more data sources from a single GraphQL API endpoint.
- Combine multiple source GraphQL APIs into a single, merged GraphQL API.
- Publish real-time data updates to your applications.
- Leverage built-in security, monitoring, logging, and tracing, with optional caching for low latency.
- Only pay for API requests and any real-time messages that are delivered.
GraphQL is basically a query language for APIs
You can use the AWS AppSync console to configure and launch a GraphQL API. GraphQL APIs generally require three components:
GraphQL schema - Your GraphQL schema is the blueprint of the API. It defines the types and fields that you can request when an operation is executed. To populate the schema with data, you must connect data sources to the GraphQL API. In this quickstart guide, we’ll be creating a schema using a predefined model.
Data sources - These are the resources that contain the data for populating your GraphQL API. This can be a DynamoDB table, Lambda function, etc. AWS AppSync supports a multitude of data sources to build robust and scalable GraphQL APIs. Data sources are linked to fields in the schema. Whenever a request is performed on a field, the data from the source populates the field. This mechanism is controlled by the resolver. In this quickstart guide, we’ll be creating a data source using a predefined model alongside the schema.
Resolvers - Resolvers are responsible for linking the schema field to the data source. They retrieve the data from the source, then return the result based on what was defined by the field. AWS AppSync supports both JavaScript and VTL for writing resolvers for your GraphQL APIs. In this quickstart guide, the resolvers will be automatically generated based on the schema and the data source. We won’t be delving into this in this section.
AWS AppSync supports the creation and configuration of all GraphQL components. When you open the console, you can use the following methods to create your API:
- Designing a customized GraphQL API by generating it through a predefined model and setting up a new DynamoDB table (data source) to support it.
- Designing a GraphQL API with a blank schema and no data sources or resolvers.
- Using a DynamoDB table to import data and generate your schema’s types and fields.
- Using AWS AppSync’s WebSocket capabilities and Pub/Sub architecture to develop real-time APIs.
- Using existing GraphQL APIs (source APIs) to link to a Merged API.
AppSync and Next.js 14
To create a graphql endpoint, you will first need Amplify cli.
npm install -g @aws-amplify/cli
In your current working directory run:
amplify init
follow the further instructions and you will have a amplify directory
To create an graphQL api, run amplify add api
1
2
3
4
5
6
7
8
? Please select from one of the below mentioned services:
> GraphQL
? Here is the GraphQL API that we will create. Select a setting to edit or continue:
> Continue
? Choose a schema template:
> Single object with fields (e.g., “Todo” with ID, name, description)
? Do you want to edit the schema now?
> Yes
The CLI should open this GraphQL schema in your text editor.
amplify/backend/api/myapi/schema.graphql
1
2
3
4
5
type Todo @model {
id: ID!
name: String!
description: String
}
Next step, deploy the endpoint using amplify push
At this step, you are asked following questions:
1
2
3
4
5
6
7
? Are you sure you want to continue? Y
? Do you want to generate code for your newly created GraphQL API? Y
? Choose the code generation language target: javascript (or your preferred language target)
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions? Y
? Enter maximum statement depth [increase from default if your schema is deeply nested]: 2
If you are using src
directory in your project, the GraphQL queries, mutations and subscriptions will be stored in it, else you can customize the file name patterns for these. I like to keep it default in src
folder.
The src folder also contains the amplifyconfiguration.json
and aws-exports.js
files, which are important and I will talk more about them further.
Configure your application
To use the graphql, use this:
1
2
3
import { Amplify, API, graphqlOperation } from "aws-amplify";
import awsconfig from "./aws-exports";
Amplify.configure(awsconfig);
App Router
We want to use one of our server components to connect to our AWS AppSync API and list out some data.
Let’s begin by creating a new utility file that will have the serverClient function that we can use to talk to our Amplify APIs on the server side.
utils/server-utils.ts
1
2
3
4
5
6
7
8
9
import { cookies } from "next/headers";
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
import config from "../../amplifyconfiguration.json";
export const serverClient = generateServerClientUsingCookies({
config,
cookies,
});
The generateServerClientUsingCookies function generates an API that can be used with Next.js server components with dynamic rendering. This creates a secure way to access Amplify APIs on the server side. The serverclient is exported so it can be used as a utility function to call our API. In our page.tsx file we will import this function and use it to list some data for our to do app.
page.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { serverClient } from "@/utils/server-utils";
import * as query from "@/graphql/queries";
export default async function Home() {
const { data, errors } = await serverClient.graphql({
query: query.listTodos,
});
if(errors){
// handle errors
}
return (
<div>
{data.listTodos.items.map((post) => {
return (
<li key={post.id}>
<div>Name: {post.name}</div>
<span>Description: {post.description}</span>
</li>
);
})}
</div>
);
Middleware
Amplify also now supports middleware in Next.js. You can use the runWithAmplifyServerContext
inside middleware to work with Amplify APIs.
Let’s build an app where you want to redirect to the login route any time a user is not authenticated. Here is an example using the app router. First, we’ll create a new server-utils file in the utils folder:
utils/server-utils.ts
1
2
3
4
5
6
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import config from "../../amplifyconfiguration.json";
export const { runWithAmplifyServerContext } = createServerRunner({
config,
});
This will create the runWithAmplifyServerContext
that we’ll be using in our middleware below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { runWithAmplifyServerContext } from "@/utils/server-utils";
// The fetchAuthSession is pulled as the server version from aws-amplify/auth/server
import { fetchAuthSession } from "aws-amplify/auth/server";
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const pathname = req.nextUrl.pathname;
const isSignInPage = pathname === "/sign-in";
const isAdminPage = pathname === "/admin" || pathname.startsWith("/admin/");
// The runWithAmplifyServerContext will run the operation below
// in an isolated matter.
const authenticated = await runWithAmplifyServerContext({
nextServerContext: { request, response },
operation: async (contextSpec) => {
try {
// The fetch will grab the session cookies
const session = await fetchAuthSession(contextSpec, {});
return session.tokens !== undefined;
} catch (error) {
console.log(error);
return false;
}
},
});
// If user is authenticated then the route request will continue on
if (authenticated) {
if (isAdminPage && authenticated.role !== "ADMIN") {
return NextResponse.redirect(new URL("/", req.nextUrl));
} else {
return response;
}
}
if (isSignInPage && authenticated) {
return NextResponse.redirect(new URL("/", req.nextUrl));
}
if (!authenticated && !isSignInPage) {
return NextResponse.redirect(new URL("/sign-in", req.nextUrl));
}
}
// This config will match all routes accept /login, /api, _next/static, /_next/image
// favicon.ico
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
"/((?!api|_next/static|_next/image|favicon.ico|login).*)",
],
};
sequenceDiagram
participant Client
participant Next.js Page
participant AWS AppSync
participant DynamoDB
Client ->> Next.js Page: Request data
Next.js Page ->> AWS AppSync: GraphQL Query
AWS AppSync ->> DynamoDB: Retrieve data
DynamoDB -->> AWS AppSync: Data
AWS AppSync -->> Next.js Page: Data
Next.js Page -->> Client: Display data