├── .gitignore ├── README.md ├── images ├── appsyncauth.png ├── appsynccognito.png ├── banner.jpg ├── cognito_create_user.png └── privileges.png ├── next-backend ├── .gitignore ├── .npmignore ├── README.md ├── bin │ └── next-backend.ts ├── cdk.json ├── graphql │ └── schema.graphql ├── jest.config.js ├── lambda-fns │ ├── Post.ts │ ├── createPost.ts │ ├── deletePost.ts │ ├── getPostById.ts │ ├── listPosts.ts │ ├── main.ts │ ├── package.json │ ├── postsByUsername.ts │ ├── updatePost.ts │ └── yarn.lock ├── lib │ └── next-backend-stack.ts ├── package.json ├── test │ └── next-backend.test.ts ├── tsconfig.json └── yarn.lock └── next-frontend ├── .gitignore ├── README.md ├── aws-exports-example.js ├── cdk-exports-example.json ├── cdk-exports.json ├── configureAmplify.js ├── graphql.js ├── package.json ├── pages ├── _app.js ├── api │ └── hello.js ├── create-post.js ├── edit-post │ └── [id].js ├── index.js ├── my-posts.js ├── posts │ └── [id].js └── profile.js ├── public ├── favicon.ico └── vercel.svg ├── schema-example.graphql ├── serverless.yml ├── styles ├── Home.module.css └── globals.css └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Full Stack Cloud Application Development on AWS with Next.js, CDK, AppSync, & Amplify 2 | 3 | ![Next.js Amplify Workshop](images/banner.jpg) 4 | 5 | In this workshop we'll build a full stack cloud application with [Next.js](https://nextjs.org/), [AWS CDK](https://aws.amazon.com/cdk/), [AWS AppSync](https://aws.amazon.com/appsync/), & [AWS Amplify](https://docs.amplify.aws/). 6 | 7 | ### Overview 8 | 9 | We'll start from scratch, creating a new Next.js app. We'll then, step by step, use CDK to build out and configure our cloud infrastructure and then use the [Amplify JS Libraries](https://github.com/aws-amplify/amplify-js) to connect the Next.js app to the APIs we create using CDK. 10 | 11 | The app will be a multi-user blogging app with a markdown editor. 12 | 13 | When you think of many types of applications like Instagram, Twitter, or Facebook, they consist of a list of items and often the ability to drill down into a single item view. The app we will be building will be very similar to this, displaying a list of posts with data like the title, content, and author of the post. 14 | 15 | We'll then add user authentication and authorization, enabling signed in users to edit and delete their own posts. 16 | 17 | This workshop should take you anywhere between 4 to 6 hours to complete. 18 | 19 | ### Environment & prerequisites 20 | 21 | Before we begin, make sure you have the following: 22 | 23 | - Node.js v10.3.0 or later installed 24 | - A valid and confirmed AWS account 25 | - You must provide IAM credentials and an AWS Region to use AWS CDK, if you have not already done so. If you have the AWS CLI installed, the easiest way to satisfy this requirement is to [install the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) and issue the following command: 26 | 27 | ```sh 28 | aws configure 29 | ``` 30 | 31 | > When configuring the IAM user, be sure to give them Administrator Access Privileges 32 | 33 | ![IAM Permissions](images/privileges.png) 34 | 35 | We will be working from a terminal using a [Bash shell](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) to run CDK CLI commands to provision infrastructure and also to run a local version of the Next.js app and test it in a web browser. 36 | 37 | > To view the CDK pre-requisite docs, click [here](https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html) 38 | 39 | ### Background needed / level 40 | 41 | This workshop is intended for intermediate to advanced JavaScript developers wanting to learn more about full stack serverless development. 42 | 43 | While some level of React and GraphQL is helpful, because all front end code provided is copy and paste-able, this workshop requires zero previous knowledge about React or GraphQL. 44 | 45 | ### Topics we'll be covering: 46 | 47 | - GraphQL API with AWS AppSync 48 | - Authentication 49 | - Authorization 50 | - Hosting 51 | - Deleting the resources 52 | 53 | ## Getting Started 54 | 55 | To get started, we first need to create a base folder for the app. Create a folder called __cdk-next__ and change into it. 56 | 57 | Next, create the Next.js app inside the __cdk-next__ directory: 58 | 59 | ```bash 60 | $ npx create-next-app next-frontend 61 | ``` 62 | 63 | Now change into the new app directory & install these dependencies using either `npm` or `yarn`: 64 | 65 | ```bash 66 | $ cd next-frontend 67 | $ npm install aws-amplify @aws-amplify/ui-react react-simplemde-editor react-markdown uuid 68 | ``` 69 | 70 | Next, change back into the root directory to begin building the back end infrastructure. 71 | 72 | ## Installing the CLI & Initializing a new CDK Project 73 | 74 | ### Installing the CDK CLI 75 | 76 | Next, we'll install the CDK CLI: 77 | 78 | ```bash 79 | $ npm install -g aws-cdk 80 | ``` 81 | 82 | ### Initializing A New Project 83 | 84 | ```bash 85 | $ mkdir next-backend 86 | $ cd next-backend 87 | $ cdk init --language=typescript 88 | ``` 89 | 90 | The CDK CLI has initialized a new project. 91 | 92 | To build the project at any time, you can run the `build` command: 93 | 94 | ```sh 95 | $ npm run build 96 | ``` 97 | 98 | #### Bootstrapping 99 | 100 | When you run `cdk bootstrap`, cdk deploys the CDK toolkit stack into an AWS environment. The `cdk bootstrap` command is run one time per account / region. 101 | 102 | If this is your first time using CDK, run the following command: 103 | 104 | ```sh 105 | $ cdk bootstrap 106 | 107 | # or 108 | 109 | $ cdk bootstrap --profile 110 | ``` 111 | 112 | #### View changes 113 | 114 | To view the resources to be deployed or changes in infrastructure at any time, you can run the CDK `diff` command: 115 | 116 | ```sh 117 | $ cdk diff 118 | ``` 119 | 120 | Next, install the CDK dependencies we'll be using using either `npm` or `yarn`: 121 | 122 | ```sh 123 | $ npm install @aws-cdk/aws-cognito @aws-cdk/aws-appsync @aws-cdk/aws-lambda @aws-cdk/aws-dynamodb 124 | ``` 125 | 126 | ## Creating the authentication service with CDK 127 | 128 | When working with CDK, the code for the main stack lives in the __lib__ directory. When we created the project, the CLI created a file named __next-backend-stack.ts__ where our stack code is written. 129 | 130 | Open the file and let's first import the constructs and classes we'll need for our project: 131 | 132 | ```typescript 133 | // lib/next-backend-stack.ts 134 | import * as cdk from '@aws-cdk/core' 135 | import * as cognito from '@aws-cdk/aws-cognito' 136 | import * as appsync from '@aws-cdk/aws-appsync' 137 | import * as ddb from '@aws-cdk/aws-dynamodb' 138 | import * as lambda from '@aws-cdk/aws-lambda' 139 | ``` 140 | 141 | Next, update the class with the following code to create the Amazon Cognito authentication service: 142 | 143 | ```typescript 144 | // lib/next-backend-stack.ts 145 | export class NextBackendStack extends cdk.Stack { 146 | constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) { 147 | super(scope, id, props) 148 | 149 | const userPool = new cognito.UserPool(this, 'cdk-blog-user-pool', { 150 | selfSignUpEnabled: true, 151 | accountRecovery: cognito.AccountRecovery.PHONE_AND_EMAIL, 152 | userVerification: { 153 | emailStyle: cognito.VerificationEmailStyle.CODE 154 | }, 155 | autoVerify: { 156 | email: true 157 | }, 158 | standardAttributes: { 159 | email: { 160 | required: true, 161 | mutable: true 162 | } 163 | } 164 | }) 165 | 166 | const userPoolClient = new cognito.UserPoolClient(this, "UserPoolClient", { 167 | userPool 168 | }) 169 | } 170 | } 171 | ``` 172 | 173 | This code will create a Cognito User Pool that will enable the user to sign in with a username, email address, and password. 174 | 175 | A `userPoolClient` will also be created enabling client applications, in our case the Next.js app, to interact with the service. 176 | 177 | ## Adding the AWS AppSync GraphQL API with CDK 178 | 179 | Next, we'll need to create the AppSync GraphQL API. 180 | 181 | To create the API, add the following code below the User Pool definition in __lib/next-backend-stack.ts__. 182 | 183 | ```typescript 184 | // lib/next-backend-stack.ts 185 | const api = new appsync.GraphqlApi(this, 'cdk-blog-app', { 186 | name: "cdk-blog-app", 187 | logConfig: { 188 | fieldLogLevel: appsync.FieldLogLevel.ALL, 189 | }, 190 | schema: appsync.Schema.fromAsset('./graphql/schema.graphql'), 191 | authorizationConfig: { 192 | defaultAuthorization: { 193 | authorizationType: appsync.AuthorizationType.API_KEY, 194 | apiKeyConfig: { 195 | expires: cdk.Expiration.after(cdk.Duration.days(365)) 196 | } 197 | }, 198 | additionalAuthorizationModes: [{ 199 | authorizationType: appsync.AuthorizationType.USER_POOL, 200 | userPoolConfig: { 201 | userPool, 202 | } 203 | }] 204 | }, 205 | }) 206 | ``` 207 | 208 | This code will create an AppSync GraphQL API with two types of authentication: API Key (public access) and Amazon Cognito User Pool (private, authenticated access). 209 | 210 | Our app will be using a combination of public and private access to achieve a common real world use case that combines the two types of access. 211 | 212 | For example, we want developers to be able to read blog posts whether they are signed in or not, but if a user is signed in, we want to give them the correct access so that they can update or delete posts that they have created (but only their own posts). 213 | 214 | ## Adding the GraphQL schema 215 | 216 | In the code where we defined the GraphQL API, we set the GraphQL schema directory as __./graphql/schema.graphql__, but we have not yet created the schema, so let's do that now. 217 | 218 | In the root of the CDK project, create a folder called __graphql__ and a file in that folder named __schema.graphql__. In that file, add the following code: 219 | 220 | ```graphql 221 | # graphql/schema.graphql 222 | type Post @aws_api_key @aws_cognito_user_pools { 223 | id: ID! 224 | title: String! 225 | content: String! 226 | owner: String! 227 | } 228 | 229 | input PostInput { 230 | id: ID 231 | title: String! 232 | content: String! 233 | } 234 | 235 | input UpdatePostInput { 236 | id: ID! 237 | title: String 238 | content: String 239 | } 240 | 241 | type Query { 242 | getPostById(postId: ID!): Post 243 | @aws_api_key @aws_cognito_user_pools 244 | listPosts: [Post]! 245 | @aws_api_key @aws_cognito_user_pools 246 | postsByUsername: [Post]! 247 | @aws_cognito_user_pools 248 | } 249 | 250 | type Mutation { 251 | createPost(post: PostInput!): Post! 252 | @aws_cognito_user_pools 253 | deletePost(postId: ID!): ID 254 | @aws_cognito_user_pools 255 | updatePost(post: UpdatePostInput!): Post 256 | @aws_cognito_user_pools 257 | } 258 | 259 | type Subscription { 260 | onCreatePost: Post! 261 | @aws_subscribe(mutations: ["createPost"]) 262 | } 263 | ``` 264 | 265 | This schema defines the `Post` type that we'll be needing along with all of the input types and operations for creating, updating, and reading `Post`s. 266 | 267 | There are also authorization rules set in place by using `@aws_api_key` and `@aws_cognito_user_pools`. 268 | 269 | __@aws_api_key__ enables public access. 270 | 271 | __@aws_cognito_user_pools__ configures private access for signed in users. 272 | 273 | You will notice that some of the queries enable both public and private access, while the mutations only allow private access. That is because we only want to enable signed in users to be able to update `Post`s, and we will even be implementing businness logic that only allows users to update `Post`s if they are the owner / creator of the `Post`. 274 | 275 | ## Adding and configuring a Lambda function data source 276 | 277 | Next, we'll create a Lambda function. The Lambda function will be the main datasource for the API, meaning we will map all of the GraphQL operations (mutations and subscriptions) into the function. 278 | 279 | The function will then call the DynamoDB database to execute of the operations we will be needing for creating, reading, updating, and deleting items in the database. 280 | 281 | To create the Lambda function, add the following code below the API definition in __lib/next-backend-stack.ts__. 282 | 283 | ```typescript 284 | // lib/next-backend-stack.ts 285 | 286 | // Create the function 287 | const postLambda = new lambda.Function(this, 'AppSyncPostHandler', { 288 | runtime: lambda.Runtime.NODEJS_14_X, 289 | handler: 'main.handler', 290 | code: lambda.Code.fromAsset('lambda-fns'), 291 | memorySize: 1024 292 | }) 293 | 294 | // Set the new Lambda function as a data source for the AppSync API 295 | const lambdaDs = api.addLambdaDataSource('lambdaDatasource', postLambda) 296 | ``` 297 | 298 | ## Adding the GraphQL resolvers 299 | 300 | Now we will create the GraphQL resolver definitions that will map the requests coming into the API into the Lambda function. 301 | 302 | To create the resolvers, add the following code below the Lambda function definition in __lib/next-backend-stack.ts__. 303 | 304 | ```typescript 305 | // lib/next-backend-stack.ts 306 | lambdaDs.createResolver({ 307 | typeName: "Query", 308 | fieldName: "getPostById" 309 | }) 310 | 311 | lambdaDs.createResolver({ 312 | typeName: "Query", 313 | fieldName: "listPosts" 314 | }) 315 | 316 | lambdaDs.createResolver({ 317 | typeName: "Query", 318 | fieldName: "postsByUsername" 319 | }) 320 | 321 | lambdaDs.createResolver({ 322 | typeName: "Mutation", 323 | fieldName: "createPost" 324 | }) 325 | 326 | lambdaDs.createResolver({ 327 | typeName: "Mutation", 328 | fieldName: "deletePost" 329 | }) 330 | 331 | lambdaDs.createResolver({ 332 | typeName: "Mutation", 333 | fieldName: "updatePost" 334 | }) 335 | ``` 336 | 337 | ## Creating the database 338 | 339 | Next, we'll create the DynamoDB table that our API will be using to store data. 340 | 341 | To create the database, add the following code below the GraphQL resolver definitions in __lib/next-backend-stack.ts__. 342 | 343 | ```typescript 344 | // lib/next-backend-stack.ts 345 | 346 | // Create the DynamoDB table 347 | const postTable = new ddb.Table(this, 'CDKPostTable', { 348 | billingMode: ddb.BillingMode.PAY_PER_REQUEST, 349 | partitionKey: { 350 | name: 'id', 351 | type: ddb.AttributeType.STRING, 352 | }, 353 | }) 354 | 355 | // Add a global secondary index to enable another data access pattern 356 | postTable.addGlobalSecondaryIndex({ 357 | indexName: "postsByUsername", 358 | partitionKey: { 359 | name: "owner", 360 | type: ddb.AttributeType.STRING, 361 | } 362 | }) 363 | 364 | // Enable the Lambda function to access the DynamoDB table (using IAM) 365 | postTable.grantFullAccess(postLambda) 366 | 367 | // Create an environment variable that we will use in the function code 368 | postLambda.addEnvironment('POST_TABLE', postTable.tableName) 369 | ``` 370 | 371 | This code will create a DynamoDB table and add a Global Secondary Index to enable us the query the table by the `username` field. 372 | 373 | We also enable permissions for the Lambda function we created earlier to be able to interact with the table as well as a new environment variable that we will be using to reference the table from within the function. 374 | 375 | ## Printing out resource values for client-side configuration 376 | 377 | If we’d like to consume the API from a client application (like we will be doing in our Next.js app), we’ll need the values of the API key, GraphQL URL, Cognito User Pool ID, Cognito User Pool Client ID, and project region to configure our app. 378 | 379 | We _could_ go inside the AWS console for each service and find these values, but CDK enables us to print these out to our terminal upon deployment as well as map these values to an output file that we can later import in our web or mobile application and use with AWS Amplify. 380 | 381 | To create these output values, add the following code below the DynamoDB table definition in __lib/next-backend-stack.ts__. 382 | 383 | ```typescript 384 | // lib/next-backend-stack.ts 385 | new cdk.CfnOutput(this, "GraphQLAPIURL", { 386 | value: api.graphqlUrl 387 | }) 388 | 389 | new cdk.CfnOutput(this, 'AppSyncAPIKey', { 390 | value: api.apiKey || '' 391 | }) 392 | 393 | new cdk.CfnOutput(this, 'ProjectRegion', { 394 | value: this.region 395 | }) 396 | 397 | new cdk.CfnOutput(this, "UserPoolId", { 398 | value: userPool.userPoolId 399 | }) 400 | 401 | new cdk.CfnOutput(this, "UserPoolClientId", { 402 | value: userPoolClient.userPoolClientId 403 | }) 404 | ``` 405 | 406 | ## Adding the Lambda function code 407 | 408 | The last thing we need to do is write the code for the Lambda function. The Lambda function will map the GraphQL operations coming in via the event into a call to the DynamoDB database. We will have functions for all of the operations defined in the GraphQL schema. The Lambda handler will read the GraphQL operation from the `event` object and call the appropriate function. 409 | 410 | Create a folder named __lambda-fns__ in the root directory of the CDK project. Next, change into this directory and initialize a new __package.json__ file and install the `uuid` library: 411 | 412 | ```sh 413 | cd lambda-fns 414 | npm init --y 415 | npm install uuid && npm install -D @types/uuid 416 | ``` 417 | 418 | In the __lambda-fns__ folder, create the following files: 419 | 420 | - Post.ts 421 | - main.ts 422 | - createPost.ts 423 | - listPosts.ts 424 | - getPostById.ts 425 | - deletePost.ts 426 | - updatePost.ts 427 | - postsByUsername.ts 428 | 429 | ### Post.ts 430 | 431 | ```typescript 432 | // lambda-fns/Post.ts 433 | type Post = { 434 | id: string, 435 | title: string, 436 | content: string, 437 | owner: string 438 | } 439 | 440 | export default Post 441 | ``` 442 | 443 | The Post type should match the GraphQL Post type and will be used in a couple of our files. 444 | 445 | ### main.ts 446 | 447 | ```typescript 448 | // lambda-fns/main.ts 449 | import createPost from './createPost' 450 | import deletePost from './deletePost' 451 | import getPostById from './getPostById' 452 | import listPosts from './listPosts' 453 | import updatePost from './updatePost' 454 | import postsByUsername from './postsByUsername' 455 | import Post from './Post' 456 | 457 | type AppSyncEvent = { 458 | info: { 459 | fieldName: string 460 | }, 461 | arguments: { 462 | postId: string, 463 | post: Post 464 | }, 465 | identity: { 466 | username: string 467 | } 468 | } 469 | 470 | exports.handler = async (event:AppSyncEvent) => { 471 | switch (event.info.fieldName) { 472 | case "getPostById": 473 | return await getPostById(event.arguments.postId) 474 | case "createPost": { 475 | const { username } = event.identity 476 | return await createPost(event.arguments.post, username) 477 | } 478 | case "listPosts": 479 | return await listPosts() 480 | case "deletePost": { 481 | const { username } = event.identity 482 | return await deletePost(event.arguments.postId, username) 483 | } 484 | case "updatePost": { 485 | const { username } = event.identity 486 | return await updatePost(event.arguments.post, username) 487 | } 488 | case "postsByUsername": { 489 | const { username } = event.identity 490 | return await postsByUsername(username) 491 | } 492 | default: 493 | return null 494 | } 495 | } 496 | ``` 497 | 498 | The handler function will use the GraphQL operation available in the `event.info.fieldname` to call the various functions that will interact with the DynamoDB database. 499 | 500 | The function will also be passed an `identity` object if the request has been authenticated by AppSync. If the event is coming from an authenticated request, then the `identity` object will be null. 501 | 502 | ### createPost.ts 503 | 504 | ```typescript 505 | // lambda-fns/createPost.ts 506 | const AWS = require('aws-sdk') 507 | const docClient = new AWS.DynamoDB.DocumentClient() 508 | import Post from './Post' 509 | import { v4 as uuid } from 'uuid' 510 | 511 | async function createPost(post: Post, username: string) { 512 | if (!post.id) { 513 | post.id = uuid() 514 | } 515 | const postData = { ...post, owner: username } 516 | const params = { 517 | TableName: process.env.POST_TABLE, 518 | Item: postData 519 | } 520 | try { 521 | await docClient.put(params).promise() 522 | return postData 523 | } catch (err) { 524 | console.log('DynamoDB error: ', err) 525 | return null 526 | } 527 | } 528 | 529 | export default createPost 530 | ``` 531 | 532 | ### listPosts.ts 533 | 534 | ```typescript 535 | // lambda-fns/listPosts.ts 536 | const AWS = require('aws-sdk') 537 | const docClient = new AWS.DynamoDB.DocumentClient() 538 | 539 | async function listPosts() { 540 | const params = { 541 | TableName: process.env.POST_TABLE, 542 | } 543 | try { 544 | const data = await docClient.scan(params).promise() 545 | return data.Items 546 | } catch (err) { 547 | console.log('DynamoDB error: ', err) 548 | return null 549 | } 550 | } 551 | 552 | export default listPosts 553 | ``` 554 | 555 | The `listPosts` function scans the database and returns all of the results in an array. 556 | 557 | ### getPostById.ts 558 | 559 | ```typescript 560 | // lambda-fns/getPostById.ts 561 | const AWS = require('aws-sdk') 562 | const docClient = new AWS.DynamoDB.DocumentClient() 563 | 564 | async function getPostById(postId: string) { 565 | const params = { 566 | TableName: process.env.POST_TABLE, 567 | Key: { id: postId } 568 | } 569 | try { 570 | const { Item } = await docClient.get(params).promise() 571 | return Item 572 | } catch (err) { 573 | console.log('DynamoDB error: ', err) 574 | } 575 | } 576 | 577 | export default getPostById 578 | ``` 579 | 580 | ### deletePost.ts 581 | 582 | ```typescript 583 | // lambda-fns/deletePost.ts 584 | const AWS = require('aws-sdk') 585 | const docClient = new AWS.DynamoDB.DocumentClient() 586 | 587 | async function deletePost(postId: string, username: string) { 588 | const params = { 589 | TableName: process.env.POST_TABLE, 590 | Key: { 591 | id: postId 592 | }, 593 | ConditionExpression: "#owner = :owner", 594 | ExpressionAttributeNames: { 595 | "#owner": "owner" 596 | }, 597 | ExpressionAttributeValues: { 598 | ':owner' : username 599 | } 600 | } 601 | try { 602 | await docClient.delete(params).promise() 603 | return postId 604 | } catch (err) { 605 | console.log('DynamoDB error: ', err) 606 | return null 607 | } 608 | } 609 | 610 | export default deletePost 611 | ``` 612 | 613 | ### updatePost.ts 614 | 615 | ```typescript 616 | // lambda-fns/updatePost.ts 617 | const AWS = require('aws-sdk') 618 | const docClient = new AWS.DynamoDB.DocumentClient() 619 | import Post from './Post' 620 | 621 | type Params = { 622 | TableName: string | undefined, 623 | Key: string | {}, 624 | ExpressionAttributeValues: any, 625 | ExpressionAttributeNames: any, 626 | ConditionExpression: string, 627 | UpdateExpression: string, 628 | ReturnValues: string, 629 | } 630 | 631 | async function updatePost(post: Post, username: string) { 632 | let params : Params = { 633 | TableName: process.env.POST_TABLE, 634 | Key: { 635 | id: post.id 636 | }, 637 | UpdateExpression: "", 638 | ConditionExpression: "#owner = :owner", 639 | ExpressionAttributeNames: { 640 | "#owner": "owner" 641 | }, 642 | ExpressionAttributeValues: { 643 | ':owner' : username 644 | }, 645 | ReturnValues: "UPDATED_NEW" 646 | } 647 | let prefix = "set " 648 | let attributes = Object.keys(post) as (keyof typeof post)[]; 649 | for (let i=0; i Note that if you run this command from another location other than the root of the CDK project, it will not work. 713 | 714 | At this point we are ready to deploy the back end. To do so, run the following command from your terminal in the root directory of your CDK project: 715 | 716 | ```sh 717 | npm run build && cdk deploy -O ../next-frontend/cdk-exports.json 718 | ``` 719 | 720 | ### Creating a user 721 | 722 | Since this is an authenticated API, we need to create a user in order to test out the API. 723 | 724 | To create a user, open the [Amazon Cognito Dashboard](https://console.aws.amazon.com/cognito) and click on __Manage User Pools__. 725 | 726 | Next, click on the User Pool that starts with __cdkbloguserpool__. _Be sure that you are in the same region in the AWS Console that you created your project in, or else the User Pool will not show up_ 727 | 728 | In this dashboard, click __Users and groups__ to create a new user. 729 | 730 | > Note that you do not need to input a phone number to create a new user. 731 | 732 | ![Create a new user](images/cognito_create_user.png) 733 | 734 | ### Testing in AppSync 735 | 736 | Now that the user is created, we can make both authenticated and unauthenticated requests in AppSync. 737 | 738 | To test out the API we can use the GraphiQL editor in the AppSync dashboard. To do so, open the [AppSync Dashboard](https://console.aws.amazon.com/appsync) and search for __cdk-blog-app__. 739 | 740 | Next, click on __Queries__ to open the query editor. 741 | 742 | Here, you will be able to choose between public access (API Key) and private access (Amazon Cognito User Pools). 743 | 744 | ![AppSync Authentication](images/appsyncauth.png) 745 | 746 | To make any authenticated requests (for mutations or querying by user ID), you will need to sign in using the user you created in the Cognito dashboard: 747 | 748 | ![AppSync Authentication](images/appsynccognito.png) 749 | 750 | > Note that the first time you sign in, you will be prompted to change your password. 751 | 752 | In the AppSync dashboard, click on __Queries__ to open the GraphiQL editor. In the editor, create a new post with the following mutation. 753 | 754 | > Be sure to have the authentication mode set to __Amazon Cognito User Pools__ in order to execute the following operations: `createPost`, `deletePost`, `updatePost`, `postsByUsername` 755 | 756 | ```graphql 757 | mutation createPost { 758 | createPost(post: { 759 | id: "001" 760 | title: "My first post" 761 | content: "Hello world!" 762 | }) { 763 | id 764 | title 765 | content 766 | owner 767 | } 768 | } 769 | ``` 770 | 771 | Then, query for the posts: 772 | 773 | ```graphql 774 | query listPosts { 775 | listPosts { 776 | id 777 | title 778 | content 779 | owner 780 | } 781 | } 782 | ``` 783 | 784 | You can also test out other operations: 785 | 786 | ```graphql 787 | mutation updatePost { 788 | updatePost(post: { 789 | id: "001" 790 | title: "My updated title" 791 | }) { 792 | id 793 | title 794 | } 795 | } 796 | 797 | query getPostById { 798 | getPostById(postId: "001") { 799 | id 800 | title 801 | content 802 | owner 803 | } 804 | } 805 | 806 | query postsByUsername { 807 | postsByUsername { 808 | id 809 | title 810 | content 811 | owner 812 | } 813 | } 814 | 815 | mutation deletePost { 816 | deletePost(postId: "001") 817 | } 818 | ``` 819 | 820 | ### Configuring the Next app 821 | 822 | Now, our API is created & we can use it in our app! 823 | 824 | The first thing we need to do is create a configuration file that we can consume in the Next.js app containing the AWS resources we just created using CDK. 825 | 826 | The CDK CLI created a new file in the root of our Next.js app called `cdk-exports.json`, located at `next-frontent/cdk-exports.json`. What we need to do next is create a file that will take these values and make them consumable by the Amplify client library. 827 | 828 | To do so, create a file named __aws-exports.js__ in the root of the __next-frontend__ directory and add the following code: 829 | 830 | ```js 831 | // next-frontend/aws-exports.js 832 | import { NextBackendStack } from './cdk-exports.json' 833 | 834 | const config = { 835 | aws_project_region: NextBackendStack.ProjectRegion, 836 | aws_user_pools_id: NextBackendStack.UserPoolId, 837 | aws_user_pools_web_client_id: NextBackendStack.UserPoolClientId, 838 | aws_appsync_graphqlEndpoint: NextBackendStack.GraphQLAPIURL, 839 | aws_appsync_apiKey: NextBackendStack.AppSyncAPIKey, 840 | aws_appsync_authenticationType: "API_KEY" 841 | } 842 | 843 | export default config 844 | ``` 845 | 846 | The next thing we need to do is to configure our Next.js app to be aware of our AWS configuration. We can do this by referencing the new `aws-exports.js` file we just created. 847 | 848 | Create a new file called __configureAmplify.js__ in the root of the Next.js app and add the following code: 849 | 850 | ```js 851 | import Amplify from 'aws-amplify' 852 | import config from './aws-exports' 853 | Amplify.configure(config) 854 | ``` 855 | 856 | Next, open __pages/\_app.js__ and import the Amplify configuration below the last import: 857 | 858 | ```js 859 | import '../configureAmplify' 860 | ``` 861 | 862 | Now, our app is ready to start using our AWS services. 863 | 864 | ### Defining the client-side GraphQL operations 865 | 866 | Next, create a file in the root of the __next-frontend__ folder named __graphql.js__. 867 | 868 | In this file, add the GraphQL operation definitions that we'll be needing for the client application: 869 | 870 | ```js 871 | export const getPostById = /* GraphQL */ ` 872 | query getPostById($postId: ID!) { 873 | getPostById(postId: $postId) { 874 | id 875 | title 876 | content 877 | owner 878 | } 879 | } 880 | ` 881 | 882 | export const listPosts = /* GraphQL */ ` 883 | query ListPosts { 884 | listPosts { 885 | id 886 | title 887 | content 888 | owner 889 | } 890 | } 891 | ` 892 | 893 | export const postsByUsername = /* GraphQL */ ` 894 | query PostsByUsername { 895 | postsByUsername { 896 | id 897 | title 898 | content 899 | owner 900 | } 901 | } 902 | ` 903 | 904 | export const createPost = /* GraphQL */ ` 905 | mutation CreatePost( 906 | $post: PostInput! 907 | ) { 908 | createPost(post: $post) { 909 | id 910 | title 911 | content 912 | owner 913 | } 914 | } 915 | ` 916 | 917 | export const updatePost = /* GraphQL */ ` 918 | mutation UpdatePost( 919 | $post: UpdatePostInput! 920 | ) { 921 | updatePost(post: $post) { 922 | id 923 | title 924 | content 925 | } 926 | } 927 | ` 928 | 929 | export const deletePost = /* GraphQL */ ` 930 | mutation DeletePost( 931 | $postId: ID! 932 | ) { 933 | deletePost(postId: $postId) 934 | } 935 | ` 936 | ``` 937 | 938 | ### Interacting with the GraphQL API from the Next.js application - Querying for data 939 | 940 | Now that everthing is set up and the API has been deployed, we can begin interacting with it. The first thing we'll do is perform a query to fetch data from our API. 941 | 942 | To do so, we need to define the query, execute the query, store the data in our state, then list the items in our UI. 943 | 944 | The main thing to notice in this component is the API call. Take a look at this piece of code: 945 | 946 | ```js 947 | /* Call API.graphql, passing in the query that we'd like to execute. */ 948 | const postData = await API.graphql({ query: listPosts }) 949 | ``` 950 | 951 | Open __pages/index.js__ and add the following code: 952 | 953 | ```js 954 | import { useState, useEffect } from 'react' 955 | import Link from 'next/link' 956 | import { API } from 'aws-amplify' 957 | import { listPosts } from '../graphql' 958 | 959 | export default function Home() { 960 | const [posts, setPosts] = useState([]) 961 | useEffect(() => { 962 | fetchPosts() 963 | }, []) 964 | async function fetchPosts() { 965 | const postData = await API.graphql({ 966 | query: listPosts 967 | }) 968 | console.log('postData: ', postData) 969 | setPosts(postData.data.listPosts) 970 | } 971 | return ( 972 |
973 |

Posts

974 | { 975 | posts.map((post, index) => ( 976 | 977 |
978 |

{post.title}

979 |

Author: {post.owner}

980 |
981 | ) 982 | ) 983 | } 984 |
985 | ) 986 | } 987 | 988 | const linkStyle = { cursor: 'pointer', borderBottom: '1px solid rgba(0, 0, 0 ,.1)', padding: '20px 0px' } 989 | const authorStyle = { color: 'rgba(0, 0, 0, .55)', fontWeight: '600' } 990 | ``` 991 | 992 | Next, start the app: 993 | 994 | ```sh 995 | $ npm run dev 996 | ``` 997 | 998 | You should be able to view the list of posts. You will not yet be able to click on a post to navigate to the detail view, that is coming up later. 999 | 1000 | ## Adding authentication 1001 | 1002 | Next, let's add a profile screen and login flow to the app. 1003 | 1004 | To do so, create a new file called __profile.js__ in the __pages__ directory. Here, add the following code: 1005 | 1006 | ```js 1007 | import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react' 1008 | import { Auth } from 'aws-amplify' 1009 | import { useState, useEffect } from 'react' 1010 | 1011 | function Profile() { 1012 | const [user, setUser] = useState(null) 1013 | useEffect(() => { 1014 | checkUser() 1015 | }, []) 1016 | async function checkUser() { 1017 | const user = await Auth.currentAuthenticatedUser() 1018 | setUser(user) 1019 | } 1020 | if (!user) return null 1021 | return ( 1022 |
1023 |

Profile

1024 |

Username: {user.username}

1025 |

Email: {user.attributes.email}

1026 | 1027 |
1028 | ) 1029 | } 1030 | 1031 | export default withAuthenticator(Profile) 1032 | ``` 1033 | 1034 | The `withAuthenticator` Amplify UI component will scaffold out an entire authentication flow to allow users to sign up and sign in. 1035 | 1036 | The `AmplifySignOut` button adds a pre-style sign out button. 1037 | 1038 | Next, add some styling to the UI component and our future rendered markdown by opening __styles/globals.css__ and adding the following code: 1039 | 1040 | ```css 1041 | :root { 1042 | --amplify-primary-color: #0072ff; 1043 | --amplify-primary-tint: #0072ff; 1044 | --amplify-primary-shade: #0072ff; 1045 | } 1046 | 1047 | pre { 1048 | background-color: #ededed; 1049 | padding: 20px; 1050 | } 1051 | 1052 | img { 1053 | max-width: 900px; 1054 | } 1055 | 1056 | a { 1057 | color: #0070f3; 1058 | } 1059 | ``` 1060 | 1061 | Next, open __pages/\_app.js__ to add some navigation and styling to be able to navigate to the new Profile page: 1062 | 1063 | ```js 1064 | import '../styles/globals.css' 1065 | import styles from '../styles/Home.module.css' 1066 | import '../configureAmplify' 1067 | import Link from 'next/link' 1068 | 1069 | function MyApp({ Component, pageProps }) { 1070 | return ( 1071 |
1072 | 1083 |
1084 | 1085 |
1086 | 1096 |
1097 | ) 1098 | } 1099 | 1100 | const navStyle = { padding: 20, borderBottom: '1px solid #ddd' } 1101 | const bodyStyle = { minHeight: 'calc(100vh - 190px)', padding: '20px 40px' } 1102 | const linkStyle = {marginRight: 20, cursor: 'pointer'} 1103 | 1104 | export default MyApp 1105 | ``` 1106 | 1107 | Next, run the app: 1108 | 1109 | ```sh 1110 | $ npm run dev 1111 | ``` 1112 | 1113 | You should now be able to sign in and view your profile, and also sign up new accounts. 1114 | 1115 | > The link to __/create-post__ will not yet work as we have not yet created this page. 1116 | 1117 | ## Making an authorized request - Creating posts 1118 | 1119 | To authenticated API calls from the Next.js client, the authorization type needs to be specified in the query or mutation. 1120 | 1121 | Here is an example of how this looks: 1122 | 1123 | ```js 1124 | const postData = await API.graphql({ 1125 | mutation: createPost, 1126 | authMode: 'AMAZON_COGNITO_USER_POOLS', 1127 | variables: { 1128 | input: postInfo 1129 | } 1130 | }) 1131 | ``` 1132 | 1133 | Let's create the page that will allow users to create a new post using an authenticated request. 1134 | 1135 | ## Adding the Create Post form and page 1136 | 1137 | Create a new page at __pages/create-post.js__ and add the following code: 1138 | 1139 | ```js 1140 | // pages/create-post.js 1141 | import { withAuthenticator } from '@aws-amplify/ui-react' 1142 | import { useState } from 'react' 1143 | import { API } from 'aws-amplify' 1144 | import { v4 as uuid } from 'uuid' 1145 | import { useRouter } from 'next/router' 1146 | import SimpleMDE from "react-simplemde-editor" 1147 | import "easymde/dist/easymde.min.css" 1148 | import { createPost } from '../graphql' 1149 | 1150 | const initialState = { title: '', content: '' } 1151 | 1152 | function CreatePost() { 1153 | const [post, setPost] = useState(initialState) 1154 | const { title, content } = post 1155 | const router = useRouter() 1156 | function onChange(e) { 1157 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 1158 | } 1159 | async function createNewPost() { 1160 | if (!title || !content) return 1161 | const id = uuid() 1162 | post.id = id 1163 | 1164 | await API.graphql({ 1165 | query: createPost, 1166 | variables: { post }, 1167 | authMode: "AMAZON_COGNITO_USER_POOLS" 1168 | }) 1169 | router.push(`/posts/${id}`) 1170 | } 1171 | return ( 1172 |
1173 |

Create new Post

1174 | 1181 | setPost({ ...post, content: value })} /> 1182 | 1183 |
1184 | ) 1185 | } 1186 | 1187 | const inputStyle = { marginBottom: 10, height: 35, width: 300, padding: 8, fontSize: 16 } 1188 | const containerStyle = { padding: '0px 40px' } 1189 | const buttonStyle = { width: 300, backgroundColor: 'white', border: '1px solid', height: 35, marginBottom: 20, cursor: 'pointer' } 1190 | 1191 | export default withAuthenticator(CreatePost) 1192 | ``` 1193 | 1194 | This will render a form and a markdown editor, allowing users to create new posts. 1195 | 1196 | Next, create a new folder in the __pages__ directory called __posts__ and a file called __[id].js__ within that folder. In __pages/posts/[id].js__, add the following code: 1197 | 1198 | ```js 1199 | // pages/posts/[id].js 1200 | import { API } from 'aws-amplify' 1201 | import { useRouter } from 'next/router' 1202 | import '../../configureAmplify' 1203 | import ReactMarkdown from 'react-markdown' 1204 | import { listPosts, getPostById } from '../../graphql' 1205 | 1206 | export default function Post({ post }) { 1207 | console.log('post: ', post) 1208 | const router = useRouter() 1209 | if (router.isFallback) { 1210 | return
Loading...
1211 | } 1212 | return ( 1213 |
1214 |

{post.title}

1215 |
1216 | 1217 |
1218 |

Created by: {post.owner}

1219 |
1220 | ) 1221 | } 1222 | 1223 | export async function getStaticPaths() { 1224 | const postData = await API.graphql({ query: listPosts }) 1225 | const paths = postData.data.listPosts.map(post => ({ params: { id: post.id }})) 1226 | return { 1227 | paths, 1228 | fallback: true, 1229 | } 1230 | } 1231 | 1232 | export async function getStaticProps ({ params }) { 1233 | const { id } = params 1234 | const postData = await API.graphql({ 1235 | query: getPostById, variables: { postId: id } 1236 | }) 1237 | return { 1238 | props: { 1239 | post: postData.data.getPostById 1240 | } 1241 | } 1242 | } 1243 | 1244 | const markdownStyle = { padding: 20, border: '1px solid #ddd', borderRadius: 5 } 1245 | ``` 1246 | 1247 | This page uses `getStaticPaths` to dynamically create pages at build time based on the posts coming back from the API. 1248 | 1249 | We also use the `fallback` flag to enable fallback routes for dynamic SSG page generation. 1250 | 1251 | `getStaticProps` is used to enable the Post data to be passed into the page as props at build time. 1252 | 1253 | ### Testing it out 1254 | 1255 | Next, run the app: 1256 | 1257 | ```sh 1258 | $ npm run dev 1259 | ``` 1260 | 1261 | You should be able to create new posts and view them dynamically. 1262 | 1263 | ### Running a build 1264 | 1265 | To run a build and test it out, run the following: 1266 | 1267 | ```sh 1268 | $ npm run build 1269 | 1270 | $ npm start 1271 | ``` 1272 | 1273 | ## Adding a filtered view for signed in user's posts 1274 | 1275 | In a future step, we will be enabling the ability to edit or delete the posts that were created by the signed in user. Before we enable that functionality, let's first create a page for only viewing the posts created by the signed in user. 1276 | 1277 | To do so, create a new file called __my-posts.js__ in the pages directory. This page will be using the `postsByUsername` query, passing in the username of the signed in user to query for only posts created by that user. 1278 | 1279 | ```js 1280 | // pages/my-posts.js 1281 | import { useState, useEffect } from 'react' 1282 | import Link from 'next/link' 1283 | import { API, Auth } from 'aws-amplify' 1284 | import { postsByUsername } from '../graphql' 1285 | 1286 | export default function MyPosts() { 1287 | const [posts, setPosts] = useState([]) 1288 | useEffect(() => { 1289 | fetchPosts() 1290 | }, []) 1291 | async function fetchPosts() { 1292 | const postData = await API.graphql({ 1293 | query: postsByUsername, 1294 | authMode: "AMAZON_COGNITO_USER_POOLS" 1295 | }) 1296 | setPosts(postData.data.postsByUsername) 1297 | } 1298 | return ( 1299 |
1300 |

My Posts

1301 | { 1302 | posts.map((post, index) => ( 1303 | 1304 |
1305 |

{post.title}

1306 |

Author: {post.owner}

1307 |
1308 | ) 1309 | ) 1310 | } 1311 |
1312 | ) 1313 | } 1314 | 1315 | const linkStyle = { cursor: 'pointer', borderBottom: '1px solid rgba(0, 0, 0 ,.1)', padding: '20px 0px' } 1316 | const authorStyle = { color: 'rgba(0, 0, 0, .55)', fontWeight: '600' } 1317 | ``` 1318 | 1319 | ### Updating the nav 1320 | 1321 | Next, we need to update the nav to show the link to the new __my-posts__ page, but only show the link if there is a signed in user. 1322 | 1323 | To do so, we'll be using a combination of the `Auth` class as well as `Hub` which allows us to listen to authentication events. 1324 | 1325 | Open __pages/\_app.js__ and make the following updates: 1326 | 1327 | 1. Import the `useState` and `useEffect` hooks from React as well as the `Auth` and `Hub` classes from AWS Amplify: 1328 | 1329 | ```js 1330 | import { useState, useEffect } from 'react' 1331 | import { Auth, Hub } from 'aws-amplify' 1332 | ``` 1333 | 1334 | 2. In the `MyApp` function, create some state to hold the signed in user state: 1335 | 1336 | ```js 1337 | const [signedInUser, setSignedInUser] = useState(false) 1338 | ``` 1339 | 1340 | 3. In the `MyApp` function, create a function to detect and maintain user state and invoke it in a `useEffect` hook: 1341 | 1342 | ```js 1343 | useEffect(() => { 1344 | authListener() 1345 | }) 1346 | async function authListener() { 1347 | Hub.listen('auth', (data) => { 1348 | switch (data.payload.event) { 1349 | case 'signIn': 1350 | return setSignedInUser(true) 1351 | case 'signOut': 1352 | return setSignedInUser(false) 1353 | } 1354 | }) 1355 | try { 1356 | await Auth.currentAuthenticatedUser() 1357 | setSignedInUser(true) 1358 | } catch (err) {} 1359 | } 1360 | ``` 1361 | 1362 | 4. In the navigation, add a link to the new route to show only if a user is currently signed in: 1363 | 1364 | ```js 1365 | { 1366 | signedInUser && ( 1367 | 1368 | My Posts 1369 | 1370 | ) 1371 | } 1372 | ``` 1373 | 1374 | > The updated component should look like [this](https://github.com/dabit3/next.js-cdk-amplify-workshop/blob/main/next-frontend/pages/_app.js) 1375 | 1376 | Next, test it out by restarting the dev server: 1377 | 1378 | ```sh 1379 | npm run dev 1380 | ``` 1381 | 1382 | ## Updating and deleting posts 1383 | 1384 | Next, let's add a way for a signed in user to edit and delete their posts. 1385 | 1386 | First, create a new folder named __edit-post__ in the __pages__ directory. Then, create a file named __[id].js__ in this folder. 1387 | 1388 | In this file, we'll be accessing the `id` of the post from a route parameter. When the component loads, we will then use the post id from the route to fetch the post data and make it available for editing. 1389 | 1390 | In this file, add the following code: 1391 | 1392 | ```js 1393 | // pages/edit-post/[id].js 1394 | import { useEffect, useState } from 'react' 1395 | import { API } from 'aws-amplify' 1396 | import { useRouter } from 'next/router' 1397 | import SimpleMDE from 'react-simplemde-editor' 1398 | import 'easymde/dist/easymde.min.css' 1399 | import { updatePost } from '../../graphql' 1400 | import { getPostById } from '../../graphql' 1401 | 1402 | function EditPost() { 1403 | const [post, setPost] = useState(null) 1404 | const router = useRouter() 1405 | const { id } = router.query 1406 | 1407 | useEffect(() => { 1408 | fetchPost() 1409 | async function fetchPost() { 1410 | if (!id) return 1411 | const postData = await API.graphql({ query: getPostById, variables: { postId: id }}) 1412 | setPost(postData.data.getPostById) 1413 | } 1414 | }, [id]) 1415 | if (!post) return null 1416 | function onChange(e) { 1417 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 1418 | } 1419 | const { title, content } = post 1420 | async function updateCurrentPost() { 1421 | if (!title || !content) return 1422 | await API.graphql({ 1423 | query: updatePost, 1424 | variables: { post: { title, content, id }}, 1425 | authMode: "AMAZON_COGNITO_USER_POOLS" 1426 | }) 1427 | console.log('post successfully updated!') 1428 | router.push('/my-posts') 1429 | } 1430 | return ( 1431 |
1432 |

Create new Post

1433 | 1440 | setPost({ ...post, content: value })} /> 1441 | 1442 |
1443 | ) 1444 | } 1445 | 1446 | const inputStyle = { marginBottom: 10, height: 35, width: 300, padding: 8, fontSize: 16 } 1447 | const containerStyle = { padding: '0px 40px' } 1448 | const buttonStyle = { width: 300, backgroundColor: 'white', border: '1px solid', height: 35, marginBottom: 20, cursor: 'pointer' } 1449 | 1450 | export default EditPost 1451 | ``` 1452 | 1453 | Next, open __pages/my-posts.js__. We'll make a few updates to this page: 1454 | 1455 | 1. Create a function for deleting a post 1456 | 2. Add a link to edit the post by navigating to `/edit-post/:postID` 1457 | 3. Add a link to view the post 1458 | 4. Create a button for deleting posts 1459 | 1460 | Update this file with the following code: 1461 | 1462 | ```js 1463 | // pages/my-posts.js 1464 | import { useState, useEffect } from 'react' 1465 | import Link from 'next/link' 1466 | import { API } from 'aws-amplify' 1467 | import { postsByUsername } from '../graphql' 1468 | import { deletePost as deletePostMutation } from '../graphql' 1469 | 1470 | export default function MyPosts() { 1471 | const [posts, setPosts] = useState([]) 1472 | useEffect(() => { 1473 | fetchPosts() 1474 | }, []) 1475 | async function fetchPosts() { 1476 | const postData = await API.graphql({ 1477 | query: postsByUsername, 1478 | authMode: "AMAZON_COGNITO_USER_POOLS" 1479 | }) 1480 | setPosts(postData.data.postsByUsername) 1481 | } 1482 | async function deletePost(postId) { 1483 | console.log("postId: ", postId) 1484 | await API.graphql({ 1485 | query: deletePostMutation, 1486 | variables: { postId }, 1487 | authMode: "AMAZON_COGNITO_USER_POOLS" 1488 | }) 1489 | fetchPosts() 1490 | } 1491 | 1492 | return ( 1493 |
1494 |

My Posts

1495 | { 1496 | posts.map((post, index) => ( 1497 |
1498 |

{post.title}

1499 |

Author: {post.owner}

1500 | Edit Post 1501 | View Post 1502 | 1506 |
1507 | ) 1508 | ) 1509 | } 1510 |
1511 | ) 1512 | } 1513 | 1514 | const buttonStyle = { cursor: 'pointer', backgroundColor: '#ddd', border: 'none', padding: '5px 20px' } 1515 | const linkStyle = { fontSize: 14, marginRight: 10 } 1516 | const itemStyle = { borderBottom: '1px solid rgba(0, 0, 0 ,.1)', padding: '20px 0px' } 1517 | const authorStyle = { color: 'rgba(0, 0, 0, .55)', fontWeight: '600' } 1518 | ``` 1519 | 1520 | ### Enabling Incremental Static Generation 1521 | 1522 | The last thing we need to do is implement [Incremental Static Generation](https://nextjs.org/docs/basic-features/data-fetching#incremental-static-regeneration). Since we are allowing users to update posts, we need to have a way for our site to render the newly updated posts. 1523 | 1524 | Incremental Static Regeneration allows you to update existing pages by re-rendering them in the background as traffic comes in. 1525 | 1526 | To enable this, open __pages/posts/[id].js__ and update the `getStaticProps` method with the following: 1527 | 1528 | ```js 1529 | export async function getStaticProps ({ params }) { 1530 | const { id } = params 1531 | const postData = await API.graphql({ 1532 | query: getPostById, variables: { postId: id } 1533 | }) 1534 | return { 1535 | props: { 1536 | post: postData.data.getPostById 1537 | }, 1538 | // Next.js will attempt to re-generate the page: 1539 | // - When a request comes in 1540 | // - At most once every second 1541 | revalidate: 100 // adds Incremental Static Generation, sets time in seconds 1542 | } 1543 | } 1544 | 1545 | ``` 1546 | 1547 | To test it out, restart the server or run a new build: 1548 | 1549 | ```sh 1550 | npm run dev 1551 | 1552 | # or 1553 | 1554 | npm run build && npm start 1555 | ``` 1556 | 1557 | ## Deployment with Serverless Framework 1558 | 1559 | To deploy to AWS, create a new file at the root of the Next.js client app called __serverless.yml__. In this file, add the following configuration: 1560 | 1561 | ```yaml 1562 | nextamplified: 1563 | component: "@sls-next/serverless-component@1.17.0" 1564 | ``` 1565 | 1566 | To deploy, run the following command from your terminal: 1567 | 1568 | ``` 1569 | npx serverless 1570 | ``` 1571 | 1572 | ## Removing Services 1573 | 1574 | To delete the frontend, run the `remove` comand: 1575 | 1576 | ```sh 1577 | cd next-frontend 1578 | npx serverless remove 1579 | ``` 1580 | 1581 | To delete the backend, run the `destroy` comand: 1582 | 1583 | ```sh 1584 | cd next-backend 1585 | cdk destroy 1586 | ``` 1587 | 1588 | ## Next steps / challenge 1589 | 1590 | 1. Enable updating posts 1591 | 2. Enable deleting posts 1592 | -------------------------------------------------------------------------------- /images/appsyncauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-cdk-amplify-workshop/6e57050e6e1505b23757edc63266d61709559691/images/appsyncauth.png -------------------------------------------------------------------------------- /images/appsynccognito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-cdk-amplify-workshop/6e57050e6e1505b23757edc63266d61709559691/images/appsynccognito.png -------------------------------------------------------------------------------- /images/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-cdk-amplify-workshop/6e57050e6e1505b23757edc63266d61709559691/images/banner.jpg -------------------------------------------------------------------------------- /images/cognito_create_user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-cdk-amplify-workshop/6e57050e6e1505b23757edc63266d61709559691/images/cognito_create_user.png -------------------------------------------------------------------------------- /images/privileges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-cdk-amplify-workshop/6e57050e6e1505b23757edc63266d61709559691/images/privileges.png -------------------------------------------------------------------------------- /next-backend/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | 10 | # Parcel default cache directory 11 | .parcel-cache 12 | 13 | .vscode -------------------------------------------------------------------------------- /next-backend/.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /next-backend/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your CDK TypeScript project! 2 | 3 | This is a blank project for TypeScript development with CDK. 4 | 5 | The `cdk.json` file tells the CDK Toolkit how to execute your app. 6 | 7 | ## Useful commands 8 | 9 | * `npm run build` compile typescript to js 10 | * `npm run watch` watch for changes and compile 11 | * `npm run test` perform the jest unit tests 12 | * `cdk deploy` deploy this stack to your default AWS account/region 13 | * `cdk diff` compare deployed stack with current state 14 | * `cdk synth` emits the synthesized CloudFormation template 15 | -------------------------------------------------------------------------------- /next-backend/bin/next-backend.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from '@aws-cdk/core'; 4 | import { NextBackendStack } from '../lib/next-backend-stack'; 5 | 6 | const app = new cdk.App(); 7 | new NextBackendStack(app, 'NextBackendStack'); 8 | -------------------------------------------------------------------------------- /next-backend/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/next-backend.ts", 3 | "context": { 4 | "@aws-cdk/core:enableStackNameDuplicates": "true", 5 | "aws-cdk:enableDiffNoFail": "true", 6 | "@aws-cdk/core:stackRelativeExports": "true", 7 | "@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /next-backend/graphql/schema.graphql: -------------------------------------------------------------------------------- 1 | type Post @aws_api_key @aws_cognito_user_pools { 2 | id: ID! 3 | title: String! 4 | content: String! 5 | owner: String! 6 | } 7 | 8 | input PostInput { 9 | id: ID 10 | title: String! 11 | content: String! 12 | } 13 | 14 | input UpdatePostInput { 15 | id: ID! 16 | title: String 17 | content: String 18 | } 19 | 20 | type Query { 21 | getPostById(postId: ID!): Post 22 | @aws_api_key @aws_cognito_user_pools 23 | listPosts: [Post] 24 | @aws_api_key @aws_cognito_user_pools 25 | postsByUsername: [Post] 26 | @aws_cognito_user_pools 27 | } 28 | 29 | type Mutation { 30 | createPost(post: PostInput!): Post 31 | @aws_cognito_user_pools 32 | deletePost(postId: ID!): ID 33 | @aws_cognito_user_pools 34 | updatePost(post: UpdatePostInput!): Post 35 | @aws_cognito_user_pools 36 | } 37 | 38 | type Subscription { 39 | onCreatePost: Post 40 | @aws_subscribe(mutations: ["createPost"]) 41 | } -------------------------------------------------------------------------------- /next-backend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /next-backend/lambda-fns/Post.ts: -------------------------------------------------------------------------------- 1 | type Post = { 2 | id: string, 3 | title: string, 4 | content: string, 5 | owner: string 6 | } 7 | 8 | export default Post -------------------------------------------------------------------------------- /next-backend/lambda-fns/createPost.ts: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk') 2 | const docClient = new AWS.DynamoDB.DocumentClient() 3 | import Post from './Post' 4 | import { v4 as uuid } from 'uuid' 5 | 6 | async function createPost(post: Post, username: string) { 7 | if (!post.id) { 8 | post.id = uuid() 9 | } 10 | const postData = { ...post, owner: username } 11 | const params = { 12 | TableName: process.env.POST_TABLE, 13 | Item: postData 14 | } 15 | try { 16 | await docClient.put(params).promise() 17 | return postData 18 | } catch (err) { 19 | console.log('DynamoDB error: ', err) 20 | return null 21 | } 22 | } 23 | 24 | export default createPost -------------------------------------------------------------------------------- /next-backend/lambda-fns/deletePost.ts: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const docClient = new AWS.DynamoDB.DocumentClient(); 3 | 4 | async function deletePost(postId: string, username: string) { 5 | const params = { 6 | TableName: process.env.POST_TABLE, 7 | Key: { 8 | id: postId 9 | }, 10 | ConditionExpression: "#owner = :owner", 11 | ExpressionAttributeNames: { 12 | "#owner": "owner" 13 | }, 14 | ExpressionAttributeValues: { 15 | ':owner' : username 16 | } 17 | }; 18 | try { 19 | await docClient.delete(params).promise() 20 | return postId 21 | } catch (err) { 22 | console.log('DynamoDB error: ', err) 23 | return null 24 | } 25 | } 26 | 27 | export default deletePost; -------------------------------------------------------------------------------- /next-backend/lambda-fns/getPostById.ts: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const docClient = new AWS.DynamoDB.DocumentClient(); 3 | 4 | async function getPostById(postId: string) { 5 | const params = { 6 | TableName: process.env.POST_TABLE, 7 | Key: { id: postId } 8 | } 9 | try { 10 | const { Item } = await docClient.get(params).promise() 11 | return Item 12 | } catch (err) { 13 | console.log('DynamoDB error: ', err) 14 | } 15 | } 16 | 17 | export default getPostById -------------------------------------------------------------------------------- /next-backend/lambda-fns/listPosts.ts: -------------------------------------------------------------------------------- 1 | 2 | const AWS = require('aws-sdk') 3 | const docClient = new AWS.DynamoDB.DocumentClient() 4 | 5 | async function listPosts() { 6 | const params = { 7 | TableName: process.env.POST_TABLE, 8 | } 9 | try { 10 | const data = await docClient.scan(params).promise() 11 | return data.Items 12 | } catch (err) { 13 | console.log('DynamoDB error: ', err) 14 | return null 15 | } 16 | } 17 | 18 | export default listPosts -------------------------------------------------------------------------------- /next-backend/lambda-fns/main.ts: -------------------------------------------------------------------------------- 1 | import createPost from './createPost' 2 | import deletePost from './deletePost' 3 | import getPostById from './getPostById' 4 | import listPosts from './listPosts' 5 | import updatePost from './updatePost' 6 | import postsByUsername from './postsByUsername' 7 | import Post from './Post' 8 | 9 | type AppSyncEvent = { 10 | info: { 11 | fieldName: string 12 | }, 13 | arguments: { 14 | postId: string, 15 | post: Post 16 | }, 17 | identity: { 18 | username: string 19 | } 20 | } 21 | 22 | exports.handler = async (event:AppSyncEvent) => { 23 | switch (event.info.fieldName) { 24 | case "getPostById": 25 | return await getPostById(event.arguments.postId) 26 | case "createPost": { 27 | const { username } = event.identity 28 | return await createPost(event.arguments.post, username) 29 | } 30 | case "listPosts": 31 | return await listPosts(); 32 | case "deletePost": { 33 | const { username } = event.identity 34 | return await deletePost(event.arguments.postId, username) 35 | } 36 | case "updatePost": { 37 | const { username } = event.identity 38 | return await updatePost(event.arguments.post, username) 39 | } 40 | case "postsByUsername": { 41 | const { username } = event.identity 42 | return await postsByUsername(username) 43 | } 44 | default: 45 | return null 46 | } 47 | } -------------------------------------------------------------------------------- /next-backend/lambda-fns/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lambda-fns", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "main.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "uuid": "^8.3.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /next-backend/lambda-fns/postsByUsername.ts: -------------------------------------------------------------------------------- 1 | 2 | const AWS = require('aws-sdk') 3 | const docClient = new AWS.DynamoDB.DocumentClient() 4 | 5 | async function postsByUsername(username: string) { 6 | const params = { 7 | TableName: process.env.POST_TABLE, 8 | IndexName: 'postsByUsername', 9 | KeyConditionExpression: '#owner = :username', 10 | ExpressionAttributeNames: { '#owner': 'owner' }, 11 | ExpressionAttributeValues: { ':username': username }, 12 | } 13 | 14 | try { 15 | const data = await docClient.query(params).promise() 16 | return data.Items 17 | } catch (err) { 18 | console.log('DynamoDB error: ', err) 19 | return null 20 | } 21 | } 22 | 23 | export default postsByUsername -------------------------------------------------------------------------------- /next-backend/lambda-fns/updatePost.ts: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk') 2 | const docClient = new AWS.DynamoDB.DocumentClient() 3 | 4 | type Params = { 5 | TableName: string | undefined, 6 | Key: string | {}, 7 | ExpressionAttributeValues: any, 8 | ExpressionAttributeNames: any, 9 | ConditionExpression: string, 10 | UpdateExpression: string, 11 | ReturnValues: string, 12 | } 13 | 14 | async function updatePost(post: any, username: string) { 15 | let params : Params = { 16 | TableName: process.env.POST_TABLE, 17 | Key: { 18 | id: post.id 19 | }, 20 | UpdateExpression: "", 21 | ConditionExpression: "#owner = :owner", 22 | ExpressionAttributeNames: { 23 | "#owner": "owner" 24 | }, 25 | ExpressionAttributeValues: { 26 | ':owner' : username 27 | }, 28 | ReturnValues: "UPDATED_NEW" 29 | } 30 | let prefix = "set "; 31 | let attributes = Object.keys(post); 32 | for (let i=0; i { 6 | const app = new cdk.App(); 7 | // WHEN 8 | const stack = new NextBackend.NextBackendStack(app, 'MyTestStack'); 9 | // THEN 10 | expectCDK(stack).to(matchTemplate({ 11 | "Resources": {} 12 | }, MatchStyle.EXACT)) 13 | }); 14 | -------------------------------------------------------------------------------- /next-backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization": false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | -------------------------------------------------------------------------------- /next-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | aws-exports.js 37 | cdk-export.json 38 | .vscode -------------------------------------------------------------------------------- /next-frontend/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | ## Learn More 18 | 19 | To learn more about Next.js, take a look at the following resources: 20 | 21 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 22 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 23 | 24 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 25 | 26 | ## Deploy on Vercel 27 | 28 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 29 | 30 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 31 | -------------------------------------------------------------------------------- /next-frontend/aws-exports-example.js: -------------------------------------------------------------------------------- 1 | import { NextBackendStack } from './cdk-exports.json' 2 | 3 | const config = { 4 | aws_project_region: NextBackendStack.ProjectRegion, 5 | aws_user_pools_id: NextBackendStack.UserPoolId, 6 | aws_user_pools_web_client_id: NextBackendStack.UserPoolClientId, 7 | aws_appsync_graphqlEndpoint: NextBackendStack.GraphQLAPIURL, 8 | aws_appsync_apiKey: NextBackendStack.AppSyncAPIKey, 9 | aws_appsync_authenticationType: "API_KEY" 10 | } 11 | 12 | export default config -------------------------------------------------------------------------------- /next-frontend/cdk-exports-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "NextBackendStack": { 3 | "UserPoolClientId": "xxxxxxxxxxxxxx", 4 | "AppSyncAPIKey": "xxx-xxxx", 5 | "UserPoolId": "us-east-1_xxxx", 6 | "GraphQLAPIURL": "https://xxxx.appsync-api.us-east-1.amazonaws.com/graphql", 7 | "ProjectRegion": "us-east-1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /next-frontend/cdk-exports.json: -------------------------------------------------------------------------------- 1 | { 2 | "NextBackendStack": { 3 | "UserPoolClientId": "2aqdb7o1ng6ieim6615vdehqlo", 4 | "AppSyncAPIKey": "da2-pjwuw6s5v5av7aw5k7l353walu", 5 | "UserPoolId": "us-east-1_Aksn8LeJb", 6 | "GraphQLAPIURL": "https://ouhtyydd45fnldwdup6jdsoo6m.appsync-api.us-east-1.amazonaws.com/graphql", 7 | "ProjectRegion": "us-east-1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /next-frontend/configureAmplify.js: -------------------------------------------------------------------------------- 1 | import Amplify from 'aws-amplify' 2 | import config from './aws-exports' 3 | Amplify.configure(config) -------------------------------------------------------------------------------- /next-frontend/graphql.js: -------------------------------------------------------------------------------- 1 | export const getPostById = /* GraphQL */ ` 2 | query getPostById($postId: ID!) { 3 | getPostById(postId: $postId) { 4 | id 5 | title 6 | content 7 | owner 8 | } 9 | } 10 | `; 11 | 12 | export const listPosts = /* GraphQL */ ` 13 | query ListPosts { 14 | listPosts { 15 | id 16 | title 17 | content 18 | owner 19 | } 20 | } 21 | `; 22 | 23 | export const postsByUsername = /* GraphQL */ ` 24 | query PostsByUsername { 25 | postsByUsername { 26 | id 27 | title 28 | content 29 | owner 30 | } 31 | } 32 | `; 33 | 34 | export const createPost = /* GraphQL */ ` 35 | mutation CreatePost( 36 | $post: PostInput! 37 | ) { 38 | createPost(post: $post) { 39 | id 40 | title 41 | content 42 | owner 43 | } 44 | } 45 | `; 46 | 47 | export const updatePost = /* GraphQL */ ` 48 | mutation UpdatePost( 49 | $post: UpdatePostInput! 50 | ) { 51 | updatePost(post: $post) { 52 | id 53 | title 54 | content 55 | } 56 | } 57 | `; 58 | 59 | export const deletePost = /* GraphQL */ ` 60 | mutation DeletePost( 61 | $postId: ID! 62 | ) { 63 | deletePost(postId: $postId) 64 | } 65 | `; 66 | -------------------------------------------------------------------------------- /next-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextamplify", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@aws-amplify/ui-react": "^0.2.31", 12 | "aws-amplify": "^3.3.11", 13 | "next": "10.0.3", 14 | "react": "17.0.1", 15 | "react-dom": "17.0.1", 16 | "react-markdown": "^5.0.3", 17 | "react-simplemde-editor": "^4.1.3", 18 | "uuid": "^8.3.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /next-frontend/pages/_app.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { Auth, Hub } from 'aws-amplify' 3 | import '../styles/globals.css' 4 | import '../configureAmplify' 5 | import Link from 'next/link' 6 | import styles from '../styles/Home.module.css' 7 | 8 | function MyApp({ Component, pageProps }) { 9 | const [signedInUser, setSignedInUser] = useState(false) 10 | useEffect(() => { 11 | authListener() 12 | }) 13 | async function authListener() { 14 | Hub.listen('auth', (data) => { 15 | switch (data.payload.event) { 16 | case 'signIn': 17 | return setSignedInUser(true) 18 | case 'signOut': 19 | return setSignedInUser(false) 20 | } 21 | }) 22 | try { 23 | await Auth.currentAuthenticatedUser() 24 | setSignedInUser(true) 25 | } catch (err) {} 26 | } 27 | return ( 28 |
29 | 47 |
48 | 49 |
50 | 60 |
61 | ) 62 | } 63 | 64 | const navStyle = { padding: 20, borderBottom: '1px solid #ddd' } 65 | const bodyStyle = { minHeight: 'calc(100vh - 190px)', padding: '20px 40px' } 66 | const linkStyle = {marginRight: 20, cursor: 'pointer'} 67 | 68 | export default MyApp -------------------------------------------------------------------------------- /next-frontend/pages/api/hello.js: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | export default (req, res) => { 4 | res.statusCode = 200 5 | res.json({ name: 'John Doe' }) 6 | } 7 | -------------------------------------------------------------------------------- /next-frontend/pages/create-post.js: -------------------------------------------------------------------------------- 1 | import { withAuthenticator } from '@aws-amplify/ui-react' 2 | import { useState } from 'react' 3 | import { API } from 'aws-amplify' 4 | import { v4 as uuid } from 'uuid' 5 | import { useRouter } from 'next/router' 6 | import SimpleMDE from "react-simplemde-editor" 7 | import "easymde/dist/easymde.min.css" 8 | import { createPost } from '../graphql' 9 | 10 | const initialState = { title: '', content: '' } 11 | 12 | function CreatePost() { 13 | const [post, setPost] = useState(initialState) 14 | const { title, content } = post 15 | const router = useRouter() 16 | function onChange(e) { 17 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 18 | } 19 | async function createNewPost() { 20 | if (!title || !content) return 21 | const id = uuid() 22 | post.id = id 23 | 24 | await API.graphql({ 25 | query: createPost, 26 | variables: { post }, 27 | authMode: "AMAZON_COGNITO_USER_POOLS" 28 | }) 29 | router.push(`/posts/${id}`) 30 | } 31 | return ( 32 |
33 |

Create new Post

34 | 41 | setPost({ ...post, content: value })} /> 42 | 43 |
44 | ) 45 | } 46 | 47 | const inputStyle = { marginBottom: 10, height: 35, width: 300, padding: 8, fontSize: 16 } 48 | const containerStyle = { padding: '0px 40px' } 49 | const buttonStyle = { width: 300, backgroundColor: 'white', border: '1px solid', height: 35, marginBottom: 20, cursor: 'pointer' } 50 | 51 | export default withAuthenticator(CreatePost) -------------------------------------------------------------------------------- /next-frontend/pages/edit-post/[id].js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { API } from 'aws-amplify' 3 | import { useRouter } from 'next/router' 4 | import SimpleMDE from 'react-simplemde-editor' 5 | import 'easymde/dist/easymde.min.css' 6 | import { updatePost } from '../../graphql' 7 | import { getPostById } from '../../graphql' 8 | 9 | function EditPost() { 10 | const [post, setPost] = useState(null) 11 | const router = useRouter() 12 | const { id } = router.query 13 | 14 | useEffect(() => { 15 | fetchPost() 16 | async function fetchPost() { 17 | if (!id) return 18 | const postData = await API.graphql({ query: getPostById, variables: { postId: id }}) 19 | setPost(postData.data.getPostById) 20 | } 21 | }, [id]) 22 | if (!post) return null 23 | function onChange(e) { 24 | setPost(() => ({ ...post, [e.target.name]: e.target.value })) 25 | } 26 | const { title, content } = post 27 | async function updateCurrentPost() { 28 | if (!title || !content) return 29 | await API.graphql({ 30 | query: updatePost, 31 | variables: { post: { title, content, id }}, 32 | authMode: "AMAZON_COGNITO_USER_POOLS" 33 | }) 34 | console.log('post successfully updated!') 35 | router.push('/my-posts') 36 | } 37 | return ( 38 |
39 |

Create new Post

40 | 47 | setPost({ ...post, content: value })} /> 48 | 49 |
50 | ) 51 | } 52 | 53 | const inputStyle = { marginBottom: 10, height: 35, width: 300, padding: 8, fontSize: 16 } 54 | const containerStyle = { padding: '0px 40px' } 55 | const buttonStyle = { width: 300, backgroundColor: 'white', border: '1px solid', height: 35, marginBottom: 20, cursor: 'pointer' } 56 | 57 | export default EditPost -------------------------------------------------------------------------------- /next-frontend/pages/index.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import Link from 'next/link' 3 | import { API } from 'aws-amplify' 4 | import { listPosts } from '../graphql' 5 | 6 | export default function Home() { 7 | const [posts, setPosts] = useState([]) 8 | useEffect(() => { 9 | fetchPosts() 10 | }, []) 11 | async function fetchPosts() { 12 | const postData = await API.graphql({ 13 | query: listPosts 14 | }) 15 | console.log('postData: ', postData) 16 | setPosts(postData.data.listPosts) 17 | } 18 | return ( 19 |
20 |

Posts

21 | { 22 | posts.map((post, index) => ( 23 | 24 |
25 |

{post.title}

26 |

Author: {post.owner}

27 |
28 | ) 29 | ) 30 | } 31 |
32 | ) 33 | } 34 | 35 | const linkStyle = { cursor: 'pointer', borderBottom: '1px solid rgba(0, 0, 0 ,.1)', padding: '20px 0px' } 36 | const authorStyle = { color: 'rgba(0, 0, 0, .55)', fontWeight: '600' } -------------------------------------------------------------------------------- /next-frontend/pages/my-posts.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import Link from 'next/link' 3 | import { API } from 'aws-amplify' 4 | import { postsByUsername } from '../graphql' 5 | import { deletePost as deletePostMutation } from '../graphql' 6 | 7 | export default function MyPosts() { 8 | const [posts, setPosts] = useState([]) 9 | useEffect(() => { 10 | fetchPosts() 11 | }, []) 12 | async function fetchPosts() { 13 | const postData = await API.graphql({ 14 | query: postsByUsername, 15 | authMode: "AMAZON_COGNITO_USER_POOLS" 16 | }) 17 | setPosts(postData.data.postsByUsername) 18 | } 19 | async function deletePost(postId) { 20 | console.log("postId: ", postId) 21 | await API.graphql({ 22 | query: deletePostMutation, 23 | variables: { postId }, 24 | authMode: "AMAZON_COGNITO_USER_POOLS" 25 | }) 26 | fetchPosts() 27 | } 28 | 29 | return ( 30 |
31 |

My Posts

32 | { 33 | posts.map((post, index) => ( 34 |
35 |

{post.title}

36 |

Author: {post.owner}

37 | Edit Post 38 | View Post 39 | 43 |
44 | ) 45 | ) 46 | } 47 |
48 | ) 49 | } 50 | 51 | const buttonStyle = { cursor: 'pointer', backgroundColor: '#ddd', border: 'none', padding: '5px 20px' } 52 | const linkStyle = { fontSize: 14, marginRight: 10 } 53 | const itemStyle = { borderBottom: '1px solid rgba(0, 0, 0 ,.1)', padding: '20px 0px' } 54 | const authorStyle = { color: 'rgba(0, 0, 0, .55)', fontWeight: '600' } -------------------------------------------------------------------------------- /next-frontend/pages/posts/[id].js: -------------------------------------------------------------------------------- 1 | import { API } from 'aws-amplify' 2 | import { useRouter } from 'next/router' 3 | import '../../configureAmplify' 4 | import ReactMarkdown from 'react-markdown' 5 | import { listPosts, getPostById } from '../../graphql' 6 | 7 | export default function Post({ post }) { 8 | console.log('post: ', post) 9 | const router = useRouter() 10 | if (router.isFallback) { 11 | return
Loading...
12 | } 13 | return ( 14 |
15 |

{post.title}

16 |
17 | 18 |
19 |

Created by: {post.owner}

20 |
21 | ) 22 | } 23 | 24 | export async function getStaticPaths() { 25 | const postData = await API.graphql({ query: listPosts }) 26 | const paths = postData.data.listPosts.map(post => ({ params: { id: post.id }})) 27 | return { 28 | paths, 29 | fallback: true, 30 | } 31 | } 32 | 33 | export async function getStaticProps ({ params }) { 34 | const { id } = params 35 | const postData = await API.graphql({ 36 | query: getPostById, variables: { postId: id } 37 | }) 38 | return { 39 | props: { 40 | post: postData.data.getPostById 41 | }, 42 | revalidate: 100 43 | } 44 | } 45 | 46 | const markdownStyle = { padding: 20, border: '1px solid #ddd', borderRadius: 5 } -------------------------------------------------------------------------------- /next-frontend/pages/profile.js: -------------------------------------------------------------------------------- 1 | import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react' 2 | import { Auth } from 'aws-amplify' 3 | import { useState, useEffect } from 'react' 4 | 5 | function Profile() { 6 | const [user, setUser] = useState(null) 7 | useEffect(() => { 8 | checkUser() 9 | }, []) 10 | async function checkUser() { 11 | const user = await Auth.currentAuthenticatedUser() 12 | setUser(user) 13 | } 14 | if (!user) return null 15 | return ( 16 |
17 |

Profile

18 |

Username: {user.username}

19 |

Email: {user.attributes.email}

20 | 21 |
22 | ) 23 | } 24 | 25 | export default withAuthenticator(Profile) -------------------------------------------------------------------------------- /next-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dabit3/next.js-cdk-amplify-workshop/6e57050e6e1505b23757edc63266d61709559691/next-frontend/public/favicon.ico -------------------------------------------------------------------------------- /next-frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /next-frontend/schema-example.graphql: -------------------------------------------------------------------------------- 1 | type Post @model 2 | @key(name: "postsByUsername", fields: ["username"], queryField: "postsByUsername") 3 | @auth(rules: [ 4 | { allow: owner, ownerField: "username" }, 5 | { allow: public, operations: [read] } 6 | ]) { 7 | id: ID! 8 | title: String! 9 | content: String! 10 | username: String 11 | } -------------------------------------------------------------------------------- /next-frontend/serverless.yml: -------------------------------------------------------------------------------- 1 | nextamplified: 2 | component: "@sls-next/serverless-component@1.17.0" -------------------------------------------------------------------------------- /next-frontend/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | min-height: 100vh; 3 | padding: 0 0.5rem; 4 | display: flex; 5 | flex-direction: column; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .main { 11 | padding: 5rem 0; 12 | flex: 1; 13 | display: flex; 14 | flex-direction: column; 15 | justify-content: center; 16 | align-items: center; 17 | } 18 | 19 | .footer { 20 | width: 100%; 21 | height: 100px; 22 | border-top: 1px solid #eaeaea; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | 28 | .footer img { 29 | margin-left: 0.5rem; 30 | } 31 | 32 | .footer a { 33 | display: flex; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .title a { 39 | color: #0070f3; 40 | text-decoration: none; 41 | } 42 | 43 | .title a:hover, 44 | .title a:focus, 45 | .title a:active { 46 | text-decoration: underline; 47 | } 48 | 49 | .title { 50 | margin: 0; 51 | line-height: 1.15; 52 | font-size: 4rem; 53 | } 54 | 55 | .title, 56 | .description { 57 | text-align: center; 58 | } 59 | 60 | .description { 61 | line-height: 1.5; 62 | font-size: 1.5rem; 63 | } 64 | 65 | .code { 66 | background: #fafafa; 67 | border-radius: 5px; 68 | padding: 0.75rem; 69 | font-size: 1.1rem; 70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 71 | Bitstream Vera Sans Mono, Courier New, monospace; 72 | } 73 | 74 | .grid { 75 | display: flex; 76 | align-items: center; 77 | justify-content: center; 78 | flex-wrap: wrap; 79 | max-width: 800px; 80 | margin-top: 3rem; 81 | } 82 | 83 | .card { 84 | margin: 1rem; 85 | flex-basis: 45%; 86 | padding: 1.5rem; 87 | text-align: left; 88 | color: inherit; 89 | text-decoration: none; 90 | border: 1px solid #eaeaea; 91 | border-radius: 10px; 92 | transition: color 0.15s ease, border-color 0.15s ease; 93 | } 94 | 95 | .card:hover, 96 | .card:focus, 97 | .card:active { 98 | color: #0070f3; 99 | border-color: #0070f3; 100 | } 101 | 102 | .card h3 { 103 | margin: 0 0 1rem 0; 104 | font-size: 1.5rem; 105 | } 106 | 107 | .card p { 108 | margin: 0; 109 | font-size: 1.25rem; 110 | line-height: 1.5; 111 | } 112 | 113 | .logo { 114 | height: 1em; 115 | } 116 | 117 | @media (max-width: 600px) { 118 | .grid { 119 | width: 100%; 120 | flex-direction: column; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /next-frontend/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | :root { 19 | --amplify-primary-color: #0072ff; 20 | --amplify-primary-tint: #0072ff; 21 | --amplify-primary-shade: #0072ff; 22 | } 23 | 24 | pre { 25 | background-color: #ededed; 26 | padding: 20px; 27 | } 28 | 29 | img { 30 | max-width: 900px; 31 | } 32 | 33 | a { 34 | color: #0070f3; 35 | } --------------------------------------------------------------------------------