One of the strengths of Next.js as a framework is that it allows you to construct both the front end and the back end for a project at the same time. Next.js applications are composed of components. In a typical project some of the components will be rendered on the back end - these are called server components.
In our next example we are going to construct a number of server components that will interact with a database. Fetching data from a database is a typical back end chore. Next.js allows us to construct server components that will fetch data from a database and then use that data to populate a component. Next.js will then serve that component up fully populated with data.
For this example project I will be using a local MySQL database to store the data for the application. This means that we will need to set up the database and employ a library that allows us to communicate with MySQL in our Next.js application.
At the top of these notes you will find both a button to download the project archive and a button to download the database files. To run the example project you will need to install those database files in MySQL. Create a new, empty schema named 'survey' in the MySQL workbench application and then import the database files.
To talk to the MySQL database we will be using the mysql2 JavaScript library. To install this library in my project I ran the command
pnpm i mysql2@latest
You will not need to do this with the project, since I have already done this step for you.
To handle the database interactions I have added a lib folder to the project and placed a data.ts file in it. That file contains code to set up the database connection and exports a set of functions to handle various database tasks.
To establish a connection to the database we run the following code:
import mysql, { ConnectionOptions, RowDataPacket } from 'mysql2/promise';
const access: ConnectionOptions = {
host: 'localhost',
port: 3306,
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
};
const conn = await mysql.createConnection(access);
To create the database connection we place some options in a ConnectionOptions object that specify which database server to connect to, what database to use, and the MySQL user name and password to use. Here I have followed a widely used convention in the Next.js world, which is to store the details of the database connection in a separate file that sets up Next.js environment variables. That file has to be placed in the app folder and named '.env'. That file contains the code that sets up the relevant environment variables:
MYSQL_DATABASE="survey" MYSQL_USER="student" MYSQL_PASSWORD="Cmsc250!"
Once we have set up the database connection we can run SQL statements. The code to run SQL on the database appears in a set of functions in data.ts.
The first of these functions fetches a list of Survey objects from the database.
export interface Survey extends RowDataPacket {
idsurvey: number;
title: string;
prompt: string;
};
export async function fetchSurveys() : Promise<Survey[]> {
const query = 'SELECT * FROM survey;';
const [surveys] = await conn.query<Survey[]>(query);
return surveys;
}
To fetch data from the database we will be using an SQL select statement. We run that SQL code by invoking the connection object's query() method. That method returns an array of RowDatePacket objects that will contain properties for each of the columns in a result row. To set all of this up we have to tell the system what columns we are expecting to get from the query by constructing a TypeScript interface that supplements the RowDataPacket with the column properties.
Also note here that all of our database interaction functions are asynchronous. Next.js expects that asynchronous functions will be called from a Next.js server component.
Now that we have a data action function that can fetch a list of surveys from the database we are going to need to construct a Next.js component to display that list of surveys. That component is the SurveyList component defined in app/ui/surveylist.tsx:
import { Survey, fetchSurveys } from '@/app/lib/data'
import Link from 'next/link'
export async function SurveyList() {
const surveys = await fetchSurveys();
return (
<div>
{ surveys.map((s) => {
return (<Link href={`/view/${s.idsurvey}`} className="block py-2 px-4 rounded">{s.title}</Link>)
})
}
</div>
);
}
What distinguishes this component as a server component is the fact that the component function in this case is declared async. Server components are allowed to invoke asychronous functions such as data actions, which is exactly what we need to do here.
Once the fetchSurveys() data action has delivered an array of Survey objects to us we can use those data objects in our component. In this case we will construct a div containing a list of Link objects that will serve as links to detail pages where we can view the contents of individual surveys.
The pages that we will be using to view individual surveys have URLs of the form /view/#, where # gets replaced with the id number of the survey we want to see questions for. This type of route is known as a dynamic route in Next.js. To handle a dynamic route we use a special app routing convention. To handle the /view/ portion of the route URL we will create a new view folder in our app folder. Inside that folder we place a folder whose name is [surveyid]. The brackets in this folder name set up a dynamic route segment that will eventually be replaced by a survey id number when we invoke the route.
Inside the [surveyid] folder we place a page.tsx file for the page that will display the details for a given survey:
import { Suspense } from 'react';
import Loading from '@/app/ui/loading';
import SurveyDetails from '@/app/ui/surveydetails';
import QuestionsList from '@/app/ui/questionslist'
export default async function SurveyPage({
params,
}: {
params: Promise<{ surveyid: number }>
}) {
const { surveyid } = await params;
return (
<Suspense fallback={<Loading/>}>
<SurveyDetails surveyid={surveyid} />
<QuestionsList surveyid={surveyid} />
</Suspense>
);
}
An important detail here is that the component defined here will be receiving a prop value that will hand the component the survey id number that we extracted from the route. We will then use that information to construct two components, one to display the title and prompt for the survey and one to display the list of questions for the given survey.
If you look in the data.ts file you will find data action functions to fetch the necessary data from the database.
Another feature I have made use of here is the Next.js Suspense component. This component is designed to hold children that have to be constructed asychronously. What those child components are still being constructed the Suspense component will show the user a fallback component. In this case we use a simple Loading component to show the user a loading message while we are waiting for the children to be constructed:
export default function Loading() {
return (<p>Loading...</p>);
}