January 28, 2023January 29, 2023 AWS DynamoDB & Nextjs Building Real World App The IT industry is booming and the technology is evolving and more technologies are getting introduced to make the life of developers easier which also allows businesses grow in virtually no time. Making a choice among such long list of technological stack is getting pretty harder and difficult for engineering teams. On the other hand, the industry still seemed to have found a particular stack that makes the product go into production in few months or even less in some cases due to which they begin focusing on the business around their product. Two such technologies are Nextjs and AWS DynamoDB. Both of which I will explain in a sec below. However, before going into these two specific technologies briefly, let’s talk about few more that are either associated with these two or I will make use of them in this article to make it convenient for me to implement this small project and explain the basic concepts. Tech Stack Here are the list of technological stack. AWS IAM User AWS DynamoDB PartiQL Nextjs Nextjs Server Routes React Query AWS[1] First comes up is the AWS. It offers several online solutions or services(more that 200) which manages large hardware infrastructure for our online products which helps teams to focus on their main product’s implementation and business aspects of their products. We are opting for DynamoDB for this article. I am not going into the details of AWS as this is a whole new world which can be explored conveniently on AWS online resources. IAM user Second comes in IAM user will allow us to authenticate with AWS DynamoDB after which we will have full access to our database table and we can perform different CRUD operations over our table. Below I will explain how to create one using AWS console shortly. If you are more interested in creating one via CLI[2] then please visit here. AWS DynamoDB[3] DynamoDB is a NoSQL database service from Amazon which provides high performance and seamless scalability to our database storages by taking care of all the administrative burdens of its hardware and the rest of the high level algorithms for us. For more details please visit Amazon DynamoDB. PartiQL[8][9] There are different approaches to interact and do operations on DynamoDB and one of them is PartiQL. It is an SQL compatible query language to perform CRUD operations on DynamoDB tables. For this blog post we are going to use PartiQL programatically using DynamoDB APIs. Nextjs[4] React offered the best possible solutions to write component based layouts however, it had some limitations such as SEO and Server Side Rendering etc which is overcame by the introduction of Nextjs. It is a react framework that helps us build static and server side rendering with high performance and SEO optimized apps and websites. To know more about it please visit nextjs.org link. Nextjs Server Routes[5] All of our app’s business logic such as interacting with DynamoDB table and data manipulating will happen at this level. Nextjs API routes help us build API in nextjs. For this we will get information from user in UI and make an http request to nextjs API where we will handle the data manipulation(if needed) and storing the data to DynamoDB table. For more information please click here. React Query[7] Another third party library we are going to utilize in this article is React Query which is optional. You can use JavaScript native fetch API however, React Query comes with some additional features such as caching, synchronizing and updating server state in react application. Now that I explained all the technologies that I am going to use in this article it is time to get our hands dirty on some practical implementation. About Project We are going to build a small calculator which helps us to calculate a percentage for our given numbers. Users will be presented a form where they will enter Marks Taken and Total Marks available. Once users enter those details they have two options, first they need to hit calculate button to calculate average and second button is Save which will store the result in AWS DynamoDB. Development Approach Below are the step wise explanation on how I will develop this small app by explaining it. Create IAM User UI or the front end implementation(with no CSS). Implementation of Nextjs API. Communicate UI and Nextjs API. Retrieve and store data to and from DynamoDB. Creating IAM User The first step is to create an IAM user on AWS using AWS console to have authenticated access to our DynamoDB table. Login to your AWS account and search for IAM in services. Click on IAM in the search result which will take you to the IAM dashboard. On the left menu, click on users. This will show us all listed users, if we have any. As you can see I already have one in the list which for privacy() reasons I am hiding it. Click on Add user button which should start a wizard to create a new user. Here enter the name for the user. I gave mine ddb-next-user for the purpose of this article. Check the checkbox for Access key – ProgrammaticAccess as we will use these information programatically to connect to DynamoDB. Click on Next: Permissions button to go to the next step. Select Attach existing policies directly and search for AmazonDynamoDBFullAccess and select this option by clicking on the checkbox to have full access to DynamoDB on the page which should look something like below. Click on Next: Tags button. This step is optional, feel free to provide any details you would like here. I left mine blank. Click on Next: Review button once you are done. Review your provided information. Once everything looks good to you then you can hit the Create user button which should give you below output on the next page. Save both of these keys for later use as we are going to use them while configuring AWS SDKs in Nextjs project which will allow us to get authenticated with AWS and thus will have full access to DynamoDB programatically. UI or the front end implementation(with no CSS). Now is the time to get our hands dirty on some coding. Go ahead and create nextjs application with the command below. npx create-next-app ddb-nextjs Now we need some dependencies to install which I elaborated above. Let’s update package.json file with the following contents. ... "dependencies": { "@aws-sdk/client-dynamodb": "^3.245.0", "@aws-sdk/lib-dynamodb": "^3.245.0", "react-query": "^3.39.2”, ... } ... And run yarn command in your terminal to install above dependencies. Now that we have the environment setup, it is time to write some code. Open /pages/index.js file and replace the existing content with below html form. import Head from 'next/head'; export default function Home() { return ( <> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <form onSubmit={handleClick}> <div> <label>Marks Taken</label> <input type="number" name="taken" /> </div> <div> <label>Available Marks</label> <input type="number" name="available" /> </div> <div> <button type="button">Calculate</button> <button>Save</button> </div> </form> </main> </> ) } Above code is pretty self explanatory as I am adding two input form fields. One for marks taken whereas the other one is for total marks. Finally I am adding two buttons one to calculate percentage and the other one to save the result to DynamoDB(which I will explain later in this article). Implementation of Nextjs API. It is time to get into the nextjs API. Nextjs make http requests to nextjs server and its APIs which resides inside /pages/api/ folder. Any file we create inside this folder becomes and API endpoint. Inside the /pages/api folder create two files get.js and save.js. Then open get.js and add below code. export default function handler(req, res) { res.status(200).json([ { name: 'Excepteur Nisi', percentage: '51', }, { name: 'Sunt Sint', percentage: '59', }, ]); } In above code, we are exporting a handler function which is sending an object as a response to the front end. It is not any fancy code yet as we are going to add DynamoDB logic here later which means we are going to come to this file and to the save.js later in this article. Communicate UI and Nextjs API. It is time to get to the fourth step in our development flow which is introducing the React Query. The way React Query works in a nutshell is useQuery creates an Observer which gets subscribed to a Query which does most of the logical part such as fetching and caching etc etc.[6] To know more about its ins and outs please go through this link where one of the co-maintainer Dominik explains React Query in more detail. Before we get going with React Query we need to import it and make the QueryClient available throughout the application using QueryProvider inside /pages/_app.js file like below. import { QueryClient, QueryClientProvider } from 'react-query'; const queryClient = new QueryClient(); export default function App({ Component, pageProps }) { return ( <QueryClientProvider client={queryClient}> <Component {...pageProps} /> </QueryClientProvider> ); } Here we are importing QueryClient and QueryClientProvider from React Query. Then we are creating a new instance of QueryClient as it holds all of the cache data and other information that we need in our components. And for that we are going to make this available throughout the application using its provider by passing it as a prop client. Now that QueryClient is available to use for all the pages, let’s go and open /pages/index.js file again and update it with the code below. import Head from 'next/head'; import { useQuery } from 'react-query'; import styles from '../styles/Home.module.css'; const fetcher = async () => { const response = await fetch(`/api/get`); return response.json(); } export default function Home() { const { data, error, isError, isLoading, } = useQuery('calculations', fetcher); if (isLoading) { return <div>Loading...</div> } if (isError) { return <div>Error! {error.message}</div> } return ( <> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <form onSubmit={handleClick}> <div> <label>Marks Taken</label> <input type="number" name="taken" /> </div> <div> <label>Available Marks</label> <input type="number" name="available" /> </div> <div> <button type="button">Calculate</button> <button>Save</button> </div> </form> <ul> { data.map((item, index) => ( <li key={index}> {item.name} has {item.percentage}% </li> )) } </ul> </main> </> ) } Here we are importing useQuery from React Query. Then we are creating a fetcher function where we are making an http request to /api/get end point which is the file we created earlier in /api/ folder. This will receive the object we are return in get.js file. ... const fetcher = async () => { const response = await fetch(`/api/get`); return response.json(); } ... The we are using useQuery hook which returns certain properties such as data, error, isError, isLoading. Data has the information returned from the api call. isError and isLoading are boolean properties which gets updated upon server error and the progress and the completion of the http request respectively. ... const { data, error, isError, isLoading, } = useQuery('calculations', fetcher); ... Then we are making use of isLoading and isError and return appropriate html with text in it. ... if (isLoading) { return <div>Loading...</div> } if (isError) { return <div>Error! {error.message}</div> } ... Later in the file, we are mapping through the data we received from the api and rendering them in list form which is the below part. ... <ul> { data.map((item, index) => ( <li key={index}> {item.name} has {item.percentage}% </li> )) } </ul> ... The fourth part is to bring in the DynamoDB. For this we need to make a post request and before that we need to calculate percentage and then when users click on the save button we will store this result in DynamoDB. First let’s write simple logic for the calculate button for simple percentage calculation. Update your index.js file with the code below. import React from ‘react’; import Head from 'next/head'; import { useQuery } from 'react-query'; import styles from '../styles/Home.module.css'; const fetcher = async () => { const response = await fetch(`/api/get`); return response.json(); } export default function Home() { const takenRef = React.useRef(); const totalRef = React.useRef(); const [resultState, setResult] = React.useState(); const { data, error, isError, isLoading, } = useQuery('calculations', fetcher); const calculate = () => { const result = Math.floor(Number(takenRef.current.value) / Number(totalRef.current.value) * 100); setResult(result); }; if (isLoading) { return <div>Loading...</div> } if (isError) { return <div>Error! {error.message}</div> } return ( <> <Head> <title>Create Next App</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <form onSubmit={handleClick}> <div> <label>Marks Taken</label> <input type="number" name="taken" /> </div> <div> <label>Available Marks</label> <input type="number" name="available" /> </div> <div> <button type=“button” onClick={calculate}>Calculate</button> <button>Save</button> </div> </form> <ul> { data.map((item, index) => ( <li key={index}> {item.name} has {item.percentage}% </li> )) } </ul> </main> </> ) } The calculate function in above code is something we are more interested in. Even before that, I imported React at the top of the file to use useRef and useState hooks. Next I am initializing both of the above hooks at the top of the functional component like below. ... const takenRef = React.useRef(); const totalRef = React.useRef(); const [resultState, setResult] = React.useState(); ... The takenRef is a reference to the marks taken by a student form input field whereas totalRef is a reference to total amount of marks form input field. Now the calculate function comes in. I am getting values from both of those inputs and divide them and multiply with 100 to get the percentage out and finally setting the result in a component state by calling setResult function which I extracted from React‘s useState hook at the top of the functional component. ... const calculate = () => { const result = Math.floor(Number(takenRef.current.value) / Number(totalRef.current.value) * 100); setResult(result); }; ... Finally I am adding references to form fields to have access to them and get the values which I am using in calculate function above. ... <div> <label>Marks Taken</label> <input ref={takenRef} type="number" name="taken" /> </div> <div> <label>Available Marks</label> <input ref={totalRef} type="number" name="available" /> </div> ... Now we are in the stage where we need to post this information to /api/save/ end point to save the result for later use. The logic for this part will go into handleClick function. Replace your code in index.js file with below code. import React from 'react'; import Head from 'next/head'; import { useQuery, useMutation } from 'react-query'; import styles from '../styles/Home.module.css'; const fetcher = async () => { const response = await fetch(`/api/get`); return response.json(); } export default function Home() { const takenRef = React.useRef(); const totalRef = React.useRef(); const [resultState, setResult] = React.useState(); const { data, error, isError, isLoading, } = useQuery('calculations', fetcher); const { isLoading: mloading, isError: mError, error: mErrorText, mutate, } = useMutation(handleClick); async function handleClick(event) { event.preventDefault(); const response = await fetch(`/api/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({result: resultState}), }); const json = await response.json(); console.log(json); }; const calculate = () => { const result = Math.floor(Number(takenRef.current.value) / Number(totalRef.current.value) * 100); setResult(result); }; if (isLoading) { return <div>Loading...</div> } if (isError) { return <div>Error! {error.message}</div> } return ( <> <Head> <title>Percentage Calcultor</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <form onSubmit={(event) => { event.preventDefault(); mutate.mutate(event); }}> <div> <label>Marks Taken</label> <input ref={takenRef} type="number" name="taken" /> </div> <div> <label>Available Marks</label> <input ref={totalRef} type="number" name="available" /> </div> <div>{resultState && <span>{resultState}</span>}</div> <div> <button type="button" onClick={calculate}>Calculate</button> <button>Save</button> </div> </form> <ul> { data.map((item, index) => ( <li key={index}> {item.name} has {item.percentage}% </li> )) } </ul> </main> </> ) } First I am importing useMutation hook from React Query. React Query provides useMutation hook that allows us to mutate data such as creating and updating etc.[10] Next I am calling the useMutation() hook inside the functional component which enables us to make POST request in this case. ... const mutate = useMutation(handleClick); ... Then inside the handleClick function I am making a post request using fetch function to /api/save end point. Setting some headers and the result body attribute of fetch function. And finally logging the response after converting the result into json. ... async function handleClick(event) { event.preventDefault(); const response = await fetch(`/api/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({result: resultState}), }); const json = await response.json(); console.log(json); }; ... onSubmit form function I am calling the mutate property of useMutation hook’s instance and passing the event object there as a parameter to have access to form data. ... <form onSubmit={(event) => { event.preventDefault(); mutate.mutate(event); }}> ... Now if you test your application both calculate and save buttons should be operational exactly as we implemented. Retrieve and store data to and from DynamoDB. The last phase where we add our result to DynamoDB. We need to create a table AWS DynamoDB. Follow the steps below to create one. Search for DynamoDB in the search box at the top once you are logged in to your AWS account and click on DynamoDB on the search result list. This should take you to below page. On this page click on Create table button. On the next page provide the name of the table and Partition Key. Choose whatever name and key you want but bear in mind we will need this information when we communicate with this table later in this article. Now click on the Create table button at the bottom will create your table and show it in the list of tables on the next page. Now let’s get back to writing code. Before we start to use DynamoDB we need to make some configuration. Create a folder with the name of config and inside that create an index.js file and place the code below. import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; const ddbClient = new DynamoDBClient({ region: 'us-east-1', credentials: { accessKeyId: PASTE_YOUR_AWS_ACCESS_KEY_ID, secretAccessKey: PASTE_YOUR_AWS_SECRET_ACCESS_KEY, }, }); const ddbDocClient = DynamoDBDocumentClient.from(ddbClient); export { ddbClient, ddbDocClient }; Here we are creating a new DynamoDBClient instance/object with the region(make sure you place the right region from your AWS console, mine is set to US East as this offers free use of DynamoDB based on my AWS account subscription which is free tier). Inside the credentials attribute paste your Access Key ID and Secret Access Key you got while creating IAM user in AWS console. Later on we are passing that object into DynamoDBDocumentClient to have access to different methods such as send in this case which we are going to make use of while saving data to DynamoDB Percentage table. Now we are good. Let’s open the save.js file and paste the code below. import { ExecuteStatementCommand } from "@aws-sdk/client-dynamodb"; import { ddbDocClient } from "../../config"; export default async function handler(req, res) { const params = { Statement: `INSERT into "Percentages" value {'name': ?, 'result': ?}`, Parameters: [{ S: req.body.result.toString() }, { S: 'name'}], }; try { const data = await ddbDocClient.send(new ExecuteStatementCommand(params)); return res.status(200).json(data); } catch (err) { console.error(err); return res.status(200).json({ error: err.status }); } } Above is a handler function for saving information to our database. First I am importing ExecuteStatementCommand which is used to execute our PartiQL query. Then we also need the configuration file which we create earlier. Inside the handler function I am setting parameters for the ExecuteStatementCommand which has a Statement attribute with a PartiQL value as a string. In this use case, I am inserting information into our database table. The second attribute is Parameters with a value of an array the information that we just received from our front end side that is the result, which is a percentage we calculated earlier and name. Here I am hardcoding the name just for demonstration purposes however, if you would like to implement that part the feel free to do so to practice. Now you should be good to go when you hit save button. Before hitting save button, make sure you calculated the percentage by providing the data the form is asking for as we are not handling and validations(which is out of the scope of this article). Now the saving functionality is out of the way it is time to retrieve that information we just saved into our database. For that, it is time to open our /api/get.js file and paste the code below. import { ExecuteStatementCommand } from "@aws-sdk/client-dynamodb"; import { ddbDocClient } from "../../config"; export default async function handler(req, res) { const params = { Statement: `SELECT * FROM "Percentages"`, }; try { const data = await ddbDocClient.send(new ExecuteStatementCommand(params)); return res.status(200).json(data); } catch (err) { console.error(err); return res.status(200).json({ error: err.status }); } } As you can see most of the code in this file is identical to save.js file with the only exception is the params object where I am using SELECT statement to retrieve data from Percentages table. We need to do a few tweaks on the front end as well. The data that is returned to the front end is inside data.Items attribute from useQuery hook. Update your ul which should look identical to below code. ... <ul> { data?.Items?.map((item, index) => ( <li key={index}> {item.result.S} has {item.name.S}% </li> )) } </ul> ... Now we are mapping through the data.Items and rendering information on S attribute of items. The complete index.js file looks something like below. import React from 'react'; import Head from 'next/head'; import { useQuery, useMutation } from 'react-query'; import styles from '../styles/Home.module.css'; const fetcher = async () => { const response = await fetch(`/api/get`); return response.json(); } export default function Home() { const takenRef = React.useRef(); const totalRef = React.useRef(); const [resultState, setResult] = React.useState(); const { data, error, isError, isLoading, } = useQuery('calculations', fetcher); const mutate = useMutation(handleClick); async function handleClick(event) { event.preventDefault(); const response = await fetch(`/api/save`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({result: resultState}), }); const json = await response.json() }; const calculate = () => { const result = Math.floor(Number(takenRef.current.value) / Number(totalRef.current.value) * 100); setResult(result); }; if (isLoading) { return <div>Loading...</div>; } if (isError) { return <div>Error! {error.message}</div>; } return ( <> <Head> <title>Percentage Calculator</title> <meta name="description" content="Generated by create next app" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <main className={styles.main}> <form onSubmit={(event) => { event.preventDefault(); mutate.mutate(event); }}> <div> <label>Marks Taken</label> <input ref={takenRef} type="number" name="taken" /> </div> <div> <label>Available Marks</label> <input ref={totalRef} type="number" name="available" /> </div> <div>{resultState && <span>{resultState}</span>}</div> <div> <button type="button" onClick={calculate}>Calculate</button> <button>Save</button> </div> </form> <ul> { data?.Items?.map((item, index) => ( <li key={index}> {item.result.S} has {item.name.S}% </li> )) } </ul> </main> </> ) } The final version of get.js file looks like this. import { ExecuteStatementCommand } from "@aws-sdk/client-dynamodb"; import { ddbDocClient } from "../../config"; export default async function handler(req, res) { const params = { Statement: `SELECT * FROM "Percentages"`, }; try { const data = await ddbDocClient.send(new ExecuteStatementCommand(params)); return res.status(200).json(data); } catch (err) { console.error(err); return res.status(200).json({ error: err.status }); } } Final version of save.js file. import { ExecuteStatementCommand } from '@aws-sdk/client-dynamodb'; import { ddbDocClient } from '../../config'; export default async function handler(req, res) { const params = { Statement: `INSERT into "Percentages" value {'name': ?, 'result': ?}`, Parameters: [{ S: req.body.result.toString() }, { S: 'name'}], }; try { const data = await ddbDocClient.send(new ExecuteStatementCommand(params)); return res.status(200).json(data); } catch (err) { console.error(err); return res.status(200).json({ error: err.status }); } } And final version of DynamoDB configuration file i.e. /config/index.js. import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; const ddbClient = new DynamoDBClient({ region: 'us-east-1', credentials: { accessKeyId: 'PASTE_YOUR_AWS_ACCESS_KEY_ID', secretAccessKey: 'PASTE_YOUR_AWS_SECRET_ACCESS_KEY', }, }); const ddbDocClient = DynamoDBDocumentClient.from(ddbClient); export { ddbClient, ddbDocClient }; Hope you got benefitted from this article. Feel free to experiment with this. If you would like to get in touch with me then have a look at my profile. Sources [1]https://aws.amazon.com/what-is-aws/?nc1=f_cc [2]https://docs.aws.amazon.com/cli/latest/userguide/cli-services-iam-new-user-group.html [3]https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html [4]https://nextjs.org/ [5]https://nextjs.org/docs/api-routes/introduction [6]https://tkdodo.eu/blog/inside-react-query [7]https://react-query-v3.tanstack.com/quick-start [8]https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ql-reference.html [9]https://partiql.org/ [10]https://tkdodo.eu/blog/mastering-mutations-in-react-query AWS DynamoDB Nextjs awsdatabasenextnextjs