DEV Community

Mo Sayed
Mo Sayed

Posted on

Next.js : A First Look

Next.js and Prisma are 2 frameworks which have received a lot of attention in recent times. To better understand what they have to offer, I used both to build a kitchen sink app, i.e. a sample application that would demonstrate the key features of each framework. Getting practical experience with new Tech is extremely useful offering a number of benefits such as

  1. Getting insight which is not always available from blogs / documentation
  2. Improved ability to make a better informed decision on whether to adopt the new technology.
  3. Developing new skills which may become highly desirable
  4. It can be fun.

The App I chose to build is a port of the infamous Spring Pet Clinic from years gone by. This reference will be familiar to developers with a background in Java & Spring. It was a sample project used to demonstrate how the same app could be built using a different set of technologies. In my case, I built it using Next.js (v12) and Prisma (v3.5), together with a Postgres database hosted in Heroku.

Image of Kitchen Sink App

The focus of this post is primarily on Next.js. Discussion of Prisma will be deferred to a future post.

What is Next.js ?

Next.js is described as

The React framework for Production

Pondering over this, raised some further questions:

What does this actually mean? Why do I need it? What does it offer over React.

To help explain this, we need to differentiate between Libraries & Frameworks. A Library is something that typically does one thing and one thing only. React is classed as a library since its main purpose is to render components in the browser. A Framework is something that does multiple things. An example of this is Angular, which not only renders components to the screen, but handles page routing, form generation & validation, fetching data, etc.

Next.js is regarded as a framework since its built on top of React and Node.js and offers out of the box services such as

  1. Server Side Rendering (SSR) - this is the ability to generate pages on the Server as opposed to the client. Two benefits of this are improved performance & better SEO.

  2. API Routes - a mechanism to build an API Layer to service the UI, enabling you to build a FullStack app rather than just a UI. It should be stressed that you can still call an existing API or build one in another tech stack and you are not constrained to using what Next.js offers.

  3. Automatic code splitting - By default Next.js splits the JavaScript for your app into separate bundles based on routes, meaning that when a User visits your site, they will only load the relevant JavaScript for the pages they visit. This will lead to significant performance improvements. Utilising dynamic imports, it's possible to achieve even finer control by implementing code splitting at the component level.

  4. Link Prefetch - When the < Link> component is used to display navigation options, Next.js will prefetch the JavaScript for each of the links. This is another performance optimisation so that when the User navigates to a page by clicking on a < Link>, the component behind it has already been fetched.

  5. File based routing - Routing in Next.js mirrors the File System, so that the path to a Page component becomes its route. This offers a more intuitive approach to routing and means that 3rd party libraries e.g. React Router are no longer required. This is most beneficial for large React applications which typically would declare their routes like

Code snippet of routes in Vanilla React app using React Router

which can be difficult to maintain as the application size grows. This becomes a non-issue with Next.js.

Many of these features address regular pain points when developing with vanilla React. Two in particular that stand out are

  1. SSR
  2. API Routes

Plain React applications built using tools like Create-React-App (CRA), utilise Client Side Rendering (CSR). This means the HTML document is generated in the browser once the JavaScript has been loaded and executed. SSR in contrast, builds the HTML document on the Server and this gets loaded onto the browser. Getting SSR to work with plain React apps requires some effort, whereas it comes out of the box for free with Next.js. Furthermore, plain React apps only provide code for rendering a UI in the browser. With Next.js we can build a fullstack application by harnessing the power of API Routes. The rest of this article will focus on these two features.

A Closer Look at Server Side Rendering (SSR)

One problem with learning a new Tech stack is that you get bombarded with a soup of vocabulary and acronyms. Next.js is no different, mentioning terms including

  • Static Site Generation (SSG)
  • Incremental Static Regeneration (ISR)
  • Server Side Rendering (SSR)

Thankfully the official documentation does a good job of explaining what these are. But I found that I benefitted further from having practical examples and adopting the following mental model:

When you build a page in Next.js, it will be pre-rendered by default. Typically each page will be composed of the following 2 elements:

Mental model for Next.js development

The Loader function for want of a better name, has 2 main responsibilities:

  1. Firstly it loads the data required to generate the Page. The data can be retrieved from a number of sources including, a database, api endpoint, local file system etc. This data is subsequently passed as props to the Render function.

  2. Secondly it is used to determine how & when the Page will be rendered, i.e. SSG vs ISR vs SSR. In practice the loader function will be one of 2 possible functions exposed by Next.js:

  • getStaticProps()

  • getServerSideProps()

The Render function looks like a typical React Functional Component and is responsible for rendering the Page. It receives as props the data returned by the loader function.

Concrete examples of the different rendering modes and their respective implementations will be given below.

Static Site Generation (SSG)

Consider, the first of the different rendering modes, Static Site Generation (SSG). In this instance, the page is rendered at build time. Effectively, when you build the app, data will be fetched and the page generated and cached. Everytime a User visits the page when the app is deployed, they will be served the same version of the page.

An example of this is the home page for the Pet Clinic app:

Home page for Pet Clinic

On this page, we display key features of the app, which are stored in a database table. This data does not change frequently. It's highly likely that it won't change for a very long time, and is therefore a very good candidate for Static Site Generation. During the build stage, code is executed to fetch content from the database and the page is generated. Thereafter everytime this page is visited, the same version of the page will be served. Note the timestamp displayed on the page. Regardless of how often the page is refreshed, the same timestamp is shown demonstrating that the page was generated at a single point in time (build) and never regenerated.

The code for this page looks like:

Code for Home Page

The key things to note here are:

  1. Line 6 - we declare the loader function. This has to be named getStaticProps as it tells the Next.js compiler that we want to render this page with SSG. Within this function we retrieve the list of features from the database using Prisma (line 8) and then return this together with the date/time the page was generated.
  2. Line 13 - we define the Rendering function. In this instance it is a React Functional Component which renders the Home page. It receives as props the data returned by getStaticProps on line 10.

A key takeaway here is that if the data/content for your page hardly ever changes, e.g. blog, website then it is a good candidate for SSG.

Incremental Static Regeneration (ISR)

SSG rendered content is fast, less susceptible to downtime and results in improved SEO; yet it suffers from a couple of problems. Firstly consider an ecommerce website with say 10,000 products; statically generating the details page for each of these products, will lead to considerably long build times, potentially hours. Secondly having to rebuild the entire website when you only want to modify a single page (i.e. fixing a typo on the home page) is highly inefficient. To address these shortcomings, Next.js offers Incremental Static Regeneration (ISR), a mechanism for regenerating static pages on a periodic basis after the app has been built. Using ISR, Next.js can determine if a page has become stale if the time elapsed between generating the page and a User requesting it exceeds some pre-defined time interval (i.e. stale time). If this is the case, Next.js will serve the stale version for the current User before regenerating the Page ready for subsequent requests.

A contrived example of this in practice is shown on the Testimonials Page. Consider if our app allows Users to submit testimonials online. The desired behaviour is to show new testimonials after they are submitted. With SSG, this would not be possible unless we rebuilt the entire site. Using ISR, we can set a stale time of say 30 seconds. Next.js will then regenerate the page when it is deemed stale.

Consider the image below, which shows the initial render of the Testimonials page:

Testimonials Page

Notice there are only 2 testimonials and the time stamp shows when the page was generated (20:08:11).

We can simulate a newly submitted testimonial, by directly entering a new row in the relevant database table and committing this change.

Inserting row into Testimonials table with Databse

After inserting the row, regardless of how many times I refresh the page, the new entry isn't shown, and the timestamp remains unchanged. However after the 30 second interval has elapsed, refreshing the page shows the new Testimonial:

Testimonial Page with new entry

Inspecting the timestamp shows a new time - 20:08:42, further indicative that the page has been re-generated.

The relevant code for this screen is shown below

Code snippet on Testimonials Page to show ISR in action

The key things to note here are:

  1. The combination of declaring getStaticProps (loader function - line 6) and the revalidate property (line 12) is instrumental in telling Next.js we wish to perform ISR. The value assigned to revalidate (30s) is used by Next.js to determine when a page has become stale and needs to be regenerated.
  2. Note that within getStaticProps, we retrieve the list of testimonials from the database using Prisma (line 8) and then return this together with the date/time the page was generated.
  3. Line 16 - declares a React Functional component that renders the Testimonials page. It receives as props the data returned by getStaticProps on line 11.

A more practical example of where ISR could be used is a Blog post with a comments section. By assigning a suitable stale time, the page can be repeatedly regenerated to show new comments that are submitted after the article has been published. Another use case where ISR can be beneficial is for an e-commerce site where content can change periodically, i.e. prices for certain items can change as part of a promotion campaign. Using ISR, these pages can be regenerated on the fly.

Server Side Rendering (SSR)

SSG is ideal when your content/data hardly changes. ISR is preferable when data/content can change but not necessarily in Real Time. To handle pages where data/content can change more dynamically, Next.js offers Server Side Rendering (SSR). This will re-generate the Page on every request. A contrived example of this is the Owner details page shown below. Every time the page is refreshed, the timestamp at the top of the page updates and the network tab shows the regenerated document returned for each request.

Owner details page showing page regeneration with SSR

The code for this page looks like:

Code snippet showing SSR in action on Owner details page

The key things to note here are:

  1. Line 10 - we declare the loader function, which has to be named getServerSideProps as this tells Next.js compiler that we want to Server Side Render the page. In this function we retrieve the details for a given Owner from the database using Prisma (line 12). This requires an Owner Id which is typically supplied in the page URL as a path parameter, e.g. owner/1985. Conveniently, Next.js passes in the path params as an argument to getServerSideProps. The Owner details are then returned together with the date/time the page was generated.
  2. Line 33 - declares a React Functional component that renders the Owner details page. It receives as props the data returned by getServerSideProps on line 29.

Client Side Rendering (CSR)

With Next.js it is still possible to perform Client Side Rendering (CSR). Consider the Owners page, where we display a list of Owners and a search box, which enables us to narrow down to specific Owner(s).

Owners List page showing all entries

When we attempt to filter Owners using the Search box,

Owners List page showing filtered results

You'll notice that there is no page reload (timestamp remains unchanged), and the only network activity is to the api endpoint that performs the filtering.

Having the ability to switch between the various rendering modes makes for a powerful framework.

API Routes

Another attractive feature of Next.js is the API Routes Layer, which allows us to build a back-end to support the UI. This means that it's possible to build a full stack application with Next.js. Consider the 'Add Owner' screen, where we can register a new Owner with the Clinic.

Create Owner Page

When the form is completed and submitted, the details would typically be sent via POST/ to a remote endpoint. With a standard React app, the endpoint would be serviced by a separate app which could be implemented using a wide range of technologies including Node.js, Java etc. With Next.js we can build a RESTful backend using API Routes alongside our UI. This makes it possible to rapidly build & deploy a Full stack application.

An example of an API Route is shown below. It provides the logic to handle the POST/ request for the above page.

Code snippet to show API Route example

The key things of interest here are:

  1. Line 1 - the api logic is encapsulated within a request handler function that takes 2 arguments, the Request & Response.
  2. Line 7 - logic to service POST/ requests such as the one we submitted on the 'Add Owner' page.
  3. Line 14 - logic to service an Update to an existing Owner. This would usually be called from a page where we can edit Owner details.
  4. Note that in each case we return a response with a status using res.status().json().

Version 12 of Next.js was recently released and introduced a middleware capability which makes it possible to intercept a Request and modify its Response. This enables us to rewrite, redirect, add headers or even stream HTML based on the user’s request.

It's worth re-iterating that with Next.js you are not forced to use API Routes to build your back-end. It's still possible to call an API that is deployed elsewhere and implemented in a different language/framework.

Discussion

Building the Pet Clinic App using Next.js & Prisma turned out to be a rather enjoyable and informative exercise, leading to a number of observations.

(1) Developer experience is actually very good. There's a small learning curve with a pre-requisite that you know React Functional Components. The major obstacle encountered learning Next.js was getting my head around the different rendering modes and when to use them. The supporting documentation is well written, sufficiently concise and accompanied by plenty of examples to learn from. As a developer I learn best from seeing code examples rather than having to read voluminous documentation. A couple of concerns I have are:

  • Co-location of client/server code within individual files; For teams divided into UI/Backend devs, this may lead to increased conflicts and collisions.
  • Frameworks tend to be opinionated and Next.js is no different. You're tied into using its File based router which may not be flexible for custom layouts. It seems possible to integrate React Router with Next.js but this requires some effort and takes the Framework outside of its comfort zone.

(2) SSR is as fast as your slowest Query. Consider a page that has 3 components, each of which make individual queries taking 1, 3 & 5 seconds respectively. We have to wait until all Queries are completed before the page can be rendered on the Server, which in this case is at least 5 seconds. However the advent of React Server Components combined with Suspense means that in the near future this may become a non-issue - It will be possible to Server render a majority of the page with loading placeholders for components with long running queries.

(3) Interestingly Next.js offers no solution to Application State Management - a key problem in Web Development. Rather a popular approach to solve this problem is to manually integrate Redux. Having an app that is Server rendered raises a question about the role of Application State Management libraries. Consider further, in recent years, there have been recommendations towards separating Client & Server state given their distinct natures:

  • Server state is asynchronous, remotely persisted & potentially out of date with shared ownership
  • Client state is synchronous, non-persistent, reliably up-to-date and client owned

Tools like React Query and SWR are better suited for handling Server state. Additionally KC Dodds presents a useful guide on how to use React for State Management, obviating the need for a 3rd party library. Given these points, we could potentially remove the need for Application State Management or reduce it's scope to simply handle Client State.

(4) Much of the discussion around Next.js is concerned with Static Websites, Blogs and e-commerce applications; there is little mention of its suitablity for building data-intensive Enterprise Applications, which can feature both static (think forms, profile page) and dynamic content (user specific data). Such applications generally would not benefit from improved SEO but rather the Performance enhancements promised by Next.js. Given that Next.js offers a hybrid approach towards rendering (e.g. CSR, SSG, ISR & SSR on per page basis), together with the ability to build a RESTful layer alongside a UI, it can certainly be used to develop Enterprise applications. As to its suitability, this will depend on a case-by-case basis. Consider that within Enterprise applications, the UI typically has to interact with a backend that

  • maybe Legacy
  • based on Micro Services Architecture
  • needs to be a Stand alone system as it services many other clients
  • involves data-exchange with a number of systems

Additionally, many backends adopt a layered approach to promote Separation of Concerns i.e. where validation, data transformation, database access are each handled separately. Trying to account for some of these in Next.js where server logic is confined to getStaticProps(), getServerSideProps() and API Routes might prove to be more challenging when dealing with complex requirements. At best, for simple backend requirements, it will be possible to build both front/back end using Next.js. For more complex requirements, Next.js can be used to build the UI which consumes the services of a separately built backend.

(5) The following graphic shows the Lighthouse scores for the Pet Clinic App. It registers high scores without any additional optimization. Lighthouse scores are a useful metric indicative of the build quality of an App and clearly Next.js helps us achieve an excellent score. This is not surprising since frameworks tend to bake in good practices and Vercel (company behind Next.js) have invested a lot of effort/money in ensuring that Next.js helps you build performant and high quality Web applications.

Lighthouse scores

Summary

This article has reported on my experience in building a sample web application using Next.js & Prisma. Focussing primarily on the former, I have found that Next.js offers a simplified and enjoyable development experience. It offers a minor learning curve, is supported by comprehensive documentation, and addresses some of the pain points of vanilla React development. Combining it with Prisma for database access enables rapid feature development promising faster time to market. Given the recent stellar hires at Vercel, one can only expect even better things from this framework.

Top comments (0)