I've chosen Next.js as my framework of choice for building full-stack these days for a number of reasons. Namely:
- It handles common things like routing, server-side rendering, and static-site generation for free.
- It's fast as hell by every metric; load time, navigation, Core Web Vitals and more.
- It's great for SEO (partially because of its speed).
- It's very well supported and keeps improving.
- It's an absolute joy to work with.
So here's part one of a crash course in the core fundamentals of Next.js to get you started working with it.
App generation with create-next-app
Generating a fresh Next.js app is easy.
# With yarn yarn create next-app my-app # With npx npx create-next-app my-app
This will generate a runnable app with a simple landing page, the dev environment, configuration, and a local Git repository.
The default language is JavaScript, which will affect the generated files and configuration. Using TypeScript is easy as well though, just add the --typescript
flag.
yarn create next-app --typescript my-app # or npx create-next-app --typescript my-app
Next.js directory structure
Here's a quick overview of some of the more relevant files and directories that create-next-app
generates:
my-app ├── styles # Put CSS styles in here to use CSS-in-JS. ├── next.config.js # Used when you want to configure Next.js's behavior. ├── pages # Each JS/TS file in here becomes a page on the site. │ └── api # Each JS/TS file in here becomes an API endpoint. ├── tsconfig.json # The generated TypeScript configuration. ├── .eslintrc.json # The generated ESLint configuration. ├── README.md # The generated README. └── public # Files added here become static assets (images, fonts, etc).
Creating pages
Filesystem-based routing in Next.js makes it incredibly easy to add a new page to your web app. Just create a mypage.js
or mypage.ts
file in the pages
directory and it will create a page on the site at /mypage
. All you need to do is export a default function that renders the content.
// // pages/mypage.js // // Add any React code you want! // export default function MyArticle() { return <h1>Hello World</h1>; }
Folders under /pages
becomes parts of the URL path as well:
// // pages/articles/my-article.js // // Visiting /articles/my-article will render this page. // export default function MyArticle() { return <h1>This is my article.</h1>; }
Creating a dynamic route
Often you'll want to have a page that takes parameters like an ID, article slug, or so on. We can define a dynamic route segment in the url by naming the file or folder with brackets ([]
) around a variable name. For instance, to handle urls of the form /articles/my-article-name
, we could add a [slug].js
file to the /articles
directory and grab the slug from the URL as a variable using the Next.js router.
// // pages/articles/[slug].js // // This could just as easily be pages/articles/[slug]/index.js // import { useRouter } from 'next/router'; export default function ArticlePage() { const router = useRouter(); return ( <h1> This is the article for {router.query.slug} </h1> ); }
Of course, you'll want to do something with the variable passed in like fetch the content from an API:
// // pages/articles/[slug].js // // This could just as easily be pages/articles/[slug]/index.js // import { useEffect, useState } from 'react'; import { useRouter } from 'next/router'; export default function ArticlePage() { const router = useRouter(); const [article, setArticle] = useState(null); useEffect(() => { async function loadArticle() { const data = await fetch(`http://my-api.com/articles/${router.query.slug}`) const json = await data.json(); setArticle(json); } loadArticle(); }, []) return ( {article && <h1>{article.title}</h1>} {article && {article.body}} ); }
Note that in the example above, you're fetching the data each time a user visits the page, which isn't optimal for SEO or page load time. Route variables can also be used to fetch data at build time (i.e., when you're deploying your site), so that the pages load statically and much faster. We do this using Next.js's static site generation feature.
Creating API routes
Creating a server-side endpoint to do backend-y things with is as simple as creating a page. Under the hood, a Next.js API route is just an Express.js route function that takes request and response objects and returns status codes and content.
To create a Next.js API route, create a file in the pages/api
directory. An extremely basic page just returns a function which returns some data. The following API endpoint would be available by making a GET request to http://localhost:3000/api/articles
:
// // pages/api/articles/index.js // export default function handler(req, res) { const articles = [ {id: 1, name: "Next.js Fundamentals", slug: "next-js-fundamentals"}, {id: 2 , name: "Modern React Component Structure", slug: "modern-react-component-structure"}, // ... ] // Send back an HTTP 200 code (success), and a JSON object. res.status(200).json(articles); }
Processing API route parameters
The req.params
object will have dynamic route variables in it:
// // pages/api/articles/[id].js // import fetch from 'node-fetch'; export default function handler(req, res) { const articleId = req.params.id; const article = await fetch(`https://jsonplaceholder.typicode.com/posts/${articleId}`) const json = await article.json(); res.status(200).json(json); }
Enforcing request method
Since API routes can respond to any request method by default (GET
, POST
, etc), you'll want to explicitly ensure each route only responds to the verb you want it to:
// // pages/api/articles/create.js // import fetch from 'node-fetch'; export default function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } // Otherwise, do something with the request body. const { title, slug, body } = req.body; const article = await fetch(`https://jsonplaceholder.typicode.com/posts//${articleId}`, { method: 'POST', body: { title, slug, body } }) res.status(200).json(json); }
Returning success/failure
Typically in real-world scenarios you'll want to return different status codes based on success or failure. Expanding on the above example:
// // pages/api/articles/create.js // import fetch from 'node-fetch'; export default function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ error: 'Method not allowed' }); } try { // Otherwise, do something with the request body. const { title, slug, body } = req.body; const article = await fetch(`https://jsonplaceholder.typicode.com/posts//${articleId}`, { method: 'POST', body: { title, slug, body } }) res.status(200).json(json); } catch (error) { // Log to Sentry or other error tracking service. // e.g., Sentry.captureException(error); // e.g., console.error(error); res.status(500).json({ error: 'Something went wrong.' }); }
Note on API architecture in Next.js
Next.js API routes are bare-bones Express.js routes; they each handle a request and return a response, but there's no implemented standard for API architecture (e.g., REST, GraphQL, etc). So if you use them as-is, it's on you to decide how you want to structure your API.
For simple apps with just a few API routes, creating your API routes ad-hoc is fine. For larger apps this can become messy, so you'll want to consider using something like Nest.js or Apollo Server in your API routes to give them some structure and consistency.
At Echobind we use Apollo Server for this purpose in Bison, our boilerplate around Next.js. We've found it to work extremely well.
Using static site generation (SSG) in Next.js
Pages are statically generated in Next.js by default; that is, as much of the page as possible is converted to static HTML when you deploy the site. This makes the pages in your Next.js load much faster, and makes them much more SEO friendly. So whenever you have content that doesn't need to change every time someone visits it, leverage SSG as much as possible.
SSG relies on two important functions in Next.js: getStaticProps
and getStaticPaths
. Both run once each time the site is deployed.
Both are async functions exported from the top-level of your page file; they're not defined within your page component.
getStaticProps()
getStaticProps
is used to calculate or fetch the data for the page at build time—from an API, the filesystem, or anywhere. Use it when any of page's data won't change from request to request.
On this site's article list page for example, I use getStaticProps() to load the article list. I don't add articles in real time—I deploy each time I write a new one. So this page only needs to regenerate when I deploy.
Here's how getStaticProps() works in this case (condensed):
// // pages/articles/index.js // import { Link } from 'components/Link'; // Note the function signature. export async function getStaticProps() { // This could pull data from an API, a database, or any other // source. It could also do and calculations or transformations // that you want to happen at build time. const data = await fetch('http://my-api.com/articles'); const articles = await data.json(); // Note that we have to return the data within the `props` property; // I forget this part now and then! return { props: { articles, }, }; } export default function ArticlesIndexPage({ // Now the articles prop is available in the page component. articles, }) { return ( <> <h1>Articles</h1> <ul> {/* Loop through and display links to each article page. */} {articles.map({ slug, id, title } => ( <li key={id}> <Link href={`/articles/${slug}`}> {title} </Link> </li> ))} </ul> </> ); }
getStaticPaths()
The getStaticPaths()
works in conjunction with getStaticProps()
for pages that have dynamic routes. It tells Next.js the full list of pages to generate static HTML for at build time.
If a page like the above /articles/[slug].js
has a dynamic route segment (the slug
), Next.js needs to know the different possible values for that segment so it can generate them. getStaticPaths()
is the place to put that information.
// // pages/articles/[slug].js // import { Link } from 'components/Link'; // Similar function signature to getStaticProps(). export async function getStaticPaths() { const data = await fetch('http://my-api.com/articles'); const articles = await data.json(); const paths = articles.map({ slug } => ({ params: { id, slug, } })); // We return a paths object instead of a props object. return { paths } } // getStaticProps() receives the paths object we generated above. export async function getStaticProps({ params: { id, slug } }) { // Get the API data for the single article, and return it as page props. const data = await fetch(`http://my-api.com/articles/${id}`); const article = await data.json() return { props: { ...article, }, }; } export default function ArticlePage({ title, body }) { return ( <> <h1>{ title }</h1> {body} </> ); }
This is part 1 in what will be a multi-part series. With the above info you can get started creating pages, API routes and static pages with dynamic data.
In the next section on Next.js basics we'll talk about some important Next.js components that help make Next.js sites fast with relative ease, like the <Link>
component and the <Image>
component.