Real-World Next.js: Build scalable, high-performance, and modern web applications using Next.js, the React framework for production by Michele Riva
Book that introduces the most important features of Next.js, and gives some examples of building applications using this framework. It assumes some familiarity with web development and React, and also discusses various other useful packages to use alongside Next.js like component libraries and testing frameworks.
Chapter 1: Next.js was developed by Vercel and improves upon React in several ways. React is entirely client-side, which can negatively affect SEO, Next.js offers features like server-side rendering, static site generation, TypeScript, prefetching, file-based routing, etc. The main difference from React is that with Next.js, a lot of things run on the server side, so client features like the browser API won’t work.
In general, Next.js uses a “convention over configuration” approach. This means there’s a conventional way to do common things without much setup. You can initialize a project using the
create-next-app tool; optionally you can configure it to use TypeScript for your project.
Chapter 2: By default, most things in Next.js use server-side rendering (SSR). The special function
getServerSideProps is used to dynamically pass data to a page that’s otherwise generated on the server side at request time.
useEffect hook to run code on the browser client side. There’s also the
process.browser variable to determine whether the current code is running on the client or server.
A third type of rendering is static site generation (SSG). Here, the entire site is generated at build time and deployed, remaining unchanged until deployed again. Next.js also supports another approach: incremental static generation (ISR). When a user accesses a page past a set time limit (eg: 600 seconds), the page is rebuilt. This method is very scalable and performant because it can be easily cached by CDNs.
Chapter 3: Next.js uses a file-based routing system. There is a
pages directory, and every file inside represents a route for the app; the
index.js page serves as the homepage. You can create dynamic routes with square brackets in the page file name, eg:
[name] to match arbitrary names, and this appears as a route variable in
getServerSideProps, or accessible inside components by importing the
For client-side navigation, use the
Link component instead of the default HTML
<a> tag, this is more optimized and performs some prefetching; alternatively, you can use
router.push for navigation after an action takes place. For serving static assets, it’s preferable to use the
Image component instead of the standard
<img> tag, this does some automatic image optimization, such as serving the image in the webp format and automatically using the
srcset property to optimize for different screen sizes.
Page metadata, crucial for SEO, is declared in the
Head component. You can use this component on any page, and it will render in that page’s head. When the same meta information is often repeated across pages, you can make a component that encapsulates this metadata.
_app.js page configures elements that should appear on every page, like a navigational bar, and can also store data meant to persist across pages, such as theme information. The
getInitialProps function runs on every page load, but it’s generally not recommended for performance. Finally, the
_document.js page adjusts the HTML and body tags, anything that isn’t part of the head.
Chapter 4: builds a sample application that uses backend APIs. Start by creating the folder structure for different levels of components based on the atomic web design principles, and for static assets, libraries, etc. Data fetching can be executed on the client or server side. Since the
fetch API is exclusive to browsers, the
axios library is useful for a consistent method of fetching data on both the server and client. The simplest use-case is retrieving data from a public API where there are no keys.
When you need to pull from a private API, it’s advised to store the API token in an environment variable placed in the
.env file, allowing for configuration between various environments. While fetching data on the client side, be aware of Cross-Origin Resource Sharing (CORS), a browser feature that might block certain requests. There’s also the concern of exposing the API key on the client side, so you should use an API page in the
pages/api directory to proxy requests to external APIs. Malicious attackers might still hit our NextJS API endpoint directly, but this will be addressed later.
Another method to interact with the backend is through GraphQL and the Apollo client. You can initialize the Apollo client once, cache it across the application, and then use it to do GraphQL queries. Some examples include queries to retrieve or modify user data.
Chapter 5: Managing Global and Local State. Local state management can be accomplished within components, where every piece of state is housed inside a component. With this system, it’s challenging to pass state from the parent to the children. For instance, in a shopping cart where each product is in a card, updating the global cart item counter isn’t straightforward when a user adjusts the quantity within the item component.
One solution is a global context wrapper. This is a wrapper declared at the top-level scope and is implicitly passed down to all components, so any component can access it. To reach this top-level context, import the
Another method is Redux: in this system, the state is global, and the reducer processes a state-action pair to generate a new state. Similar to the context method, there’s a top-level provider in
_app.js that makes the state globally accessible. When a user clicks a button, it triggers a dispatch with an action that the reducer then manages to update the global state.
Chapter 6. Different ways of styling: Styled JSX lets you put CSS directly into the component, and automatically limits the scope to the component. This is a CSS-in-JS approach, but a downside is that it often doesn’t have good IDE support when CSS is written directly in JS files.
CSS modules solve this: they are still scoped to a component but place all of the CSS code in its own file with suffix
.module.css. It compiles CSS modules using PostCSS, which is a tool to smooth over some browser incompatibilities. This can also be combined with SASS and SCSS, which offer extended features like nesting selectors.
Chapter 7. UI Frameworks, this chapter builds the same application using Chakra UI and Tailwind. Chakra UI is a component library with React style components for different UI elements; it has layout components like Grid and Center, and contains many props to control layout and styling properties. You can pass the theme config with a provider at the top level, and it exposes a hook to toggle between light and dark color schemes.
Tailwind is a utility-first CSS framework that is framework agnostic. To control themes, you can use the
next-themes package, which is similar to Chakra UI and exposes a provider at the top level, along with hooks to change the theme at the click of a button. It offers various classes to manage styling and layout, including responsive layout. The server side remains the same between these two libraries; only the styling and layout differ. If you prefer components over CSS classes, there’s Headless UI, which has React-style components that are unstyled but can be styled with Tailwind.
Chapter 8. Custom servers are useful when you want part of your app to be served using Next.js and other parts with something else. One way to do this is with Express.js, where you write rules for handling different routes and pass them to Next.js for some routes but not others; as long as the route is handled by Next.js, all of the Next.js features will work, including
getServerSideProps. One thing to note is that you have to include the
_next folder, which contains static assets that Next.js requires to run. Another alternative is Fastify, this is similar to Express.js but slightly easier to set up and handles the static asset issue automatically. However, when using custom servers, you cannot deploy to some providers like Vercel and Netlify, which only support Next.js.
Chapter 9. Testing: There are several types of tests. Unit tests focus on individual functions and can be tested using packages like Jest, for functions such as trimming a string. For testing components, a popular package is
react-testing-library, which can render a component with props and verify the properties of the generated DOM, ensuring that different elements exist and display the correct information. JSDOM is useful for emulating some browser features for testing purposes, ensuring that the component renders correctly inside tests.
End-to-end testing checks if requests to a server return the right data and status code. A good package for these tests is Cypress: you can set up the package.json file to automatically start the server and run the Cypress test suite. It can simulate clicks and test navigation works correctly.
Chapter 10. Search engine optimization (SEO). Recall the three options for rendering a site: static site generation (SSG), server-side rendering (SSR), and client-side rendering (CSR), each has different trade-offs for SEO vs dynamic capabilities. Static generation is the best for SEO because all content is generated statically, and performance will be optimal. Client-side rendering might hide many elements from search engine crawlers and is the worst for SEO; server-side rendering is a middle ground. Private pages don’t need SEO when login is required.
SEO depends on several factors. The page routing structures should have readable text in the URL, the right meta data, and performant components like images. HTML tags should be meaningful, reflecting the proper structure. Additionally, pages should load quickly and avoid layout shift after loading, these negatively impact SEO scores. To monitor performance, the
reportWebVitals function is a useful tool that provides a dashboard for these metrics. If using Vercel, this is automatically available.
Chapter 11. Deployment: A popular method for deploying Next.js is using Vercel, which operates on a serverless function, so server-side rendered pages run without maintaining an always-on server, reducing costs. Vercel can link to a GitHub repository for automatic deployment.
A CDN (Content Delivery Network) is beneficial if the entire site is static, offering the best performance by serving content from the region closest to the user. AWS is the option that is most flexible but requires more setup. Alternatively, Vercel and Cloudflare can watch GitHub branches and are simpler to set up.
It’s also possible to deploy Next.js on any server, but some additional configurations needed, such as installing dependencies, using a process manager for restarts in case of crashes, setting up a reverse proxy like NGINX, implementing a firewall for security, etc. For repetitive deployments, Docker is a practical tool to streamline the process.
Chapter 12. Authentication and Session Management. After a user has logged in, either with a password, social login, or SSO, a session is used to keep track of this information so that they don’t have to log in again on every page. In a server-side session, the client helps maintain this data, and a cookie links the client session to the server session. This method is stateful, so it’s harder to manage and deploy. Conversely, stateless sessions don’t require the server to track session state; instead, the client passes a token with each request, which the server validates.
JSON Web Tokens (JWT) are composed of three parts: the first part is the header, containing the algorithm used; the second is the payload, holding the data encoded in base64. This data is easily decodable, so sensitive information shouldn’t be stored here. The third segment is the signature, a cryptographic stamp that allows the server to confirm the content’s authenticity. The server signs the JWT using a secret key only it knows, ensuring the JWT is not tampered with.
To implement custom authentication, the server verifies the username and password, which we assume is hardcoded. The server then signs a JWT and returns it to the client, containing the logged-in user’s information. The browser receives the JWT and saves it as a session cookie for all subsequent requests. The server then checks the signature to prevent clients from masquerading as different users. In some cases, it’s beneficial to allow public access to specific data (like blog posts) for anonymous users and search crawlers, requiring authentication only for certain actions, such as posting comments.
Generally, it’s not recommended to design your own authentication system. Using providers like Auth0 is simpler. To set up Auth0, you specify valid callback URLs where users will be directed after login or logout. You set up some secret keys as environment variables, and Auth0 will set up some endpoints on the
/api/ routes to handle authentication. From there, you can wrap the entire app in a UserContext to make user information available, and within components, use the the
useUser function to fetch user details.
Chapter 13. This chapter builds an e-commerce website using GraphCMS. GraphCMS provides an easy GraphQL interface for the frontend to retrieve information about products and prices, so we only need to build the frontend. We statically generate all of the product pages using GraphQL code to fetch all the products and then render each product page.
There’s a cart feature to manage purchase items, implemented as a global cart context wrapper. On the checkout page, we query GraphCMS again to retrieve all the details of the cart items. This information is then sent to Stripe to manage payment, the data includes a list of purchase items with their quantities, prices, shipping information, etc. Stripe then directs the client to callback URLs for both successful and canceled transactions. Note that Next.js provides a template called Next.js Commerce, which you can use to spin up an e-commerce site with minimal development effort.