Full-fledged full stack with Next.js

12.01.2022 Tom Trapp
Mobile nextjs javascript frontend framework handson howto

In this TechUp we want to get to know NextJS, try out the concepts and possibilities it offers and give an overview of the different deployment options.

Next, Nest, Nuxt... Basically, there are various frameworks that build on NodeJS and want to bring JavaScript, TypeScript or another frontend language into the backend.

Here is a brief overview to avoid confusion.

  • NestJs - server side applications, Typescript, APIs
  • NuxtJs - Plugin for VueJS, Developer Experience, sustainable development
  • NextJs - The React Framework For Production React, SEO optimized, SSR for React

NextJs

What is NextJS?

NextJS is an extension for React that claims to offer the best developer experience with the features needed for production. Technically, NextJS builds on Node.js and extends React with server-side rendering and static side generation. Vercel is the company behind NextJS, the first release was published in 2016.

In the following we will get to know some central functions of NextJS, but the framework offers far more functionality such as an e-commerce starter pack, code splitting and other useful features.

What problem does it solve?

Simply put, Next.js solves recurring SEO problems in React applications by using Server Side Rendering. Basically, a React project (as well as other frontend frameworks) uses a client side rendering (CSR) approach (more below).

SEO technically, this can lead to two problems:

  • Content cannot always be indexed correctly by SEO bots because bots need to be able to interpret and execute JavaScript
  • The First Contentful Paint is slow because everything has to be assembled in the client first

To eliminate these problems, there are generally different rendering strategies on the web. NextJS supports three of them out-of-the-box. These concepts are fundamentally independent of NextJS or React and can also be found in other technologies.

In our example, we want to query and display ToDos from a residual endpoint, a simple use case. As always, the code samples can be found at GitHub.

Rendering Strategies

CSR - Client Side Rendering

As the first Rendering Strategy, let's take a closer look at Client Side Rendering. It should be mentioned here that NextJS does not support this approach out of the box, but for the sake of completeness we will nevertheless briefly discuss it.

CSR - Client Side Rendering

With CSR, the client gets a wrapper HTML from the browser, which has no content yet. JavaScript files such as a client-side JavaScript framework such as React are then loaded via this HTML. React then queries all dynamic data, assembles the content, renders the page, and makes the page interactive. This approach is found in most frontend frameworks.

Deployment

Deployment of this type is quite simple, since no computing power is required. Classically you have a folder with static HTML, CSS & JS files and have to host them.

Known ways to deploy this:

  • Amplify
  • Digital Ocean App 'Static Site'
  • Static web hosting or CDN (e.g. AWS S3 Bucket)

SSG - Static Site Generation

SSG - Static Site Generation

SSG means the complete generation of all resource & pages at build time. At runtime, only HTML files are loaded from a web server or a CDN.

This means that every time the content of the page is changed, the entire project has to be rebuilt.

Here you also often hear the term Static Site Generator or JAMstack, more about that in our TechUp how Headless-CMS works with JAMstack.

Technically, at build time the server is started and all known routes are called and their HTML is extracted. This also means that all surrounding systems are queried at build time.

Are there further URLs on a page for e.g. detail pages, these are also called up. In the case of an online shop, all linked product detail pages are also queried and statically extracted. Depending on the size of the project, this can lead to long build times.

Of course, you still have the option of loading custom Javascript or even a framework at runtime and being able to react to user interactions. This loading of frameworks at runtime 'on-top' is called Hydration.

Deployment

The deployment of this type is also quite simple, since no computing power is required. Classically you have a folder with static HTML, CSS & JS files and have to host them. The possibilities are identical to those of the CSR approach.

NextJS implementation

Branch: ssg

NextJS offers this functionality out-of-the-box.

The getStaticProps method is used to tell NextJS that this is an SSG page and should fetch the data at build time.

// This function gets called at build time
export async function getStaticProps() {
    const res = await fetch('https://jsonplaceholder.typicode.com/todos')
    const todos = await res.json()
    return {
        props: {
            todos,
        },
    }
}

For this purpose, the build command must be adjusted in package.json:

next build && next export

With export you tell the Next.js CLI that all pages should be output as Static-Sites and placed in the folder out/.

We can then build our SSG project via npm run build. Fortunately, NextJS shows us exactly which of these types it is in the build.

npm run build

> build
> next build && next export

info  - Checking validity of types  
info  - Creating an optimized production build  
info  - Compiled successfully
info  - Collecting page data  
info  - Generating static pages (5/5)
info  - Finalizing page optimization  

Page                                       Size     First Load JS
┌ ○ /                                      239 B          71.7 kB
├   /_app                                  0 B            71.5 kB
├ ○ /404                                   194 B          71.7 kB
├ λ /api/hello                             0 B            71.5 kB
├ ● /ssg                                   307 B          71.8 kB
└ ○ /static                                3.29 kB        74.8 kB
+ First Load JS shared by all              71.5 kB
  ├ chunks/framework-6e4ba497ae0c8a3f.js   42 kB
  ├ chunks/main-deb592798b94b511.js        28.2 kB
  ├ chunks/pages/_app-9cd1d19dd7237c4c.js  493 B
  ├ chunks/webpack-514908bffb652963.js     770 B
  └ css/2974ebc8daa97b04.css               209 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

...

To test our result, we need to start a local HTTP server after the build & export so that the files can be accessed.

npm install http-server -g
http-server -p 8080 -c-1 out/ -o

We can then call up the URL http://127.0.0.1:8080/ssg and see in the source text that the finished HTML comes back from the HTTP server with all the content of the rest of the endpoint. Here the API endpoint data was retrieved at build time.

It is important here that this type cannot be mixed with other types (for example with ssr), otherwise build errors will occur with next export. This means that all pages of the complete project have to be generated in the SSG approach (with next export) at build time.

This approach offers a great advantage if the actual content does not change often. For example, super cool to build and publish a blog. Of course, you also have the option here of using the Hydration process to load a frontend framework at runtime and only load individual data (e.g. the current price of the product).

SSR - Server side rendering

SSR - Server side rendering

SSR is exactly the opposite of SSG, the pages are generated on the server at request time. This means that the client gets the rendered HTML page with all content from the server. We already know this approach from backend or MVC development.

Here the load on the client side is shifted to the server side, with each request peripheral systems are queried again to assemble the HTML page. This approach is used especially for very volatile content with many moving parts, which are even different for each user.

Deployment

It is important to note here that you need computing power, since you need a running server that takes over the assembly of the HTML page.

Known ways to deploy this:

  • Amplify in combination with CloudFront & Lambdba@Edge
  • Digital Ocean App 'Web Service'
  • Netlify
  • Node JS Server
  • Docker Image

Basically, for this type of deployment you need either a hosting provider that supports NodeJS directly or which Docker container.

NextJS implementation

Branch: ssr

In NextJS, this way is activated by default. Similar to the SSG approach, NextJS also offers a method to control this behavior. The method getServerSideProps is thus called at the request time.

// This function gets called at request time
export async function getServerSideProps() {
    const res = await fetch('https://jsonplaceholder.typicode.com/todos')
    const todos = await res.json()
    return {
        props: {
            todos,
        },
    }
}

Here it is nice to see that the actual implementation is identical to the SSG approach, only the method name changes. When building, you don't need a special target here, you can start a dev server immediately via next dev. If you want to build the artifact and then start the productive profile, use next build && next start.

We can do this via npm as follows:

npm run build
npm run start

We can then call up our SSR page via http://localhost:3000/ssr, which is reassembled with each request. Since no caching is implemented, we also queried the API endpoint from the server with every request.

If we take a closer look at the build output, one special feature quickly becomes apparent:

Page                                       Size     First Load JS
┌ ○ /                                      239 B          71.7 kB
├   /_app                                  0 B            71.5 kB
├ ○ /404                                   194 B          71.7 kB
├ λ /api/hello                             0 B            71.5 kB
├ ● /ssg                                   307 B          71.8 kB
├ λ /ssr                                   306 B          71.8 kB
└ ○ /static                                3.29 kB        74.8 kB
+ First Load JS shared by all              71.5 kB
  ├ chunks/framework-6e4ba497ae0c8a3f.js   42 kB
  ├ chunks/main-deb592798b94b511.js        28.2 kB
  ├ chunks/pages/_app-9cd1d19dd7237c4c.js  493 B
  ├ chunks/webpack-514908bffb652963.js     770 B
  └ css/2974ebc8daa97b04.css               209 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)
○  (Static)  automatically rendered as static HTML (uses no initial props)
●  (SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

We still have our /ssg page in the project, the build was successful. This is one of the great advantages of NextJS, SSR & SSG can be mixed (but now via next export). At build time our SSG page has now been called and built and placed in the .next/server/pages folder in the project. This allows us to assemble parts of our application statically at build time and other pages dynamically at request time.

Of course, you can also react to user interactions here via Hydration using JavaScript or similar.

ISR - Incremental Static Regeneration

ISR - Incremental Static Regeneration

As the project gets bigger and bigger, both of the previous ways have their drawbacks and pitfalls. For example, if the online shop has 200,000 products and one product requires approx. 50 MS build time, the build will run for more than 2 hours. Depending on the number of users, an SSR approach could lead to load and performance problems.

In principle, an improvement could be achieved here with a custom implemented caching, but there is also a better way!

Incremental Static Regeneration or ISR for short allows statically built pages to be updated at runtime. We can control exactly which pages should be updated and when.

Deployment

The requirements for deployment here are the same as for the SSR approach, since computing power is also required here.

NextJS Implementation

Revalidate

Branch: isr

We activate ISR by packing the revalidate property into the getStaticProps method.

Here we say after more than 10 seconds the page should be generated again. Using the timestamp, we see at runtime that the page is updated after the initial 10 seconds. Before that we see the SSG version of the page that was built at build time.

// This function gets called at build and request time
export async function getStaticProps() {
    const res = await fetch('https://jsonplaceholder.typicode.com/todos')
    const todos = await res.json()
    const now = Date.now()
    return {
        props: {
            todos,
            now,
        },
        revalidate: 10
    }
}

Again, it is nice to see that only one property is added and the actual implementation remains the same.

After these 10 seconds, the cache is updated and all users get the same updated version from the cache for at least the next 10 seconds. As soon as a user calls up the page again, the page is rendered again once server-side, and the result is placed back in the cache.

In order to check this, we have to start our server with the productive profile, so we use the following commands as with SSR:

npm run build
npm run start

The build output correctly displays ISR, among other things:

Page                                       Size     First Load JS
├ ● /isr (ISR: 10 Seconds)                 339 B          71.8 kB

...

   (ISR)     incremental static regeneration (uses revalidate in getStaticProps)

We can then call up the page as usual under http://localhost:3000/isr, we see that we get the complete HTML back from the server, as with the SSR. With a refresh after some time, we then see that the timestamp changes, the page has been generated again. If we then immediately reload the page (within 10 seconds) the display remains the same, we have the new, cached version.

Now we have learned how to update static pages again after a certain time.

GetStaticRoutes

The build for 200,000 products would still take a long time, but NextJS also offers a clever solution here.

Using the getStaticPaths method, we can control which pages should be built at build time. In combination with revalidate, we can control exactly which pages should be built at build time and which should be cached at request time for how long. This allows us to play around with the build time and cache hit rate.

// pages/products/[id].js

export async function getStaticPaths() {
    const products = await getTop1000Products();
    const paths = products.map((product) => ({
        params: { id: product.id },
    }));

    return { paths, fallback: 'blocking' };
}

Example from https://vercel.com/docs/concepts/next.js/incremental-static-regeneration

In the above code snippet we are in a parameterizable route, which is controlled via the ID parameter. From a technical point of view, this is the product detail page, which is assembled using the ID.

We call up the 1000 most popular products here and have them built statically via SSG, the remaining products are built and cached via SSR & ISR at request time. This speeds up the build enormously, but we still use all the ISR advantages.

Using fallback: blocking one says that for the first user who calls up a product outside the 'Top 1000', the page server-side is rendered and then placed in the cache. Alternatively, a waiting page could be displayed to the user, the page is then automatically reloaded after successful generation.

Deploy Docker

Of course, a NextJS application can also be containerized and deployed via Docker, e.g. in a Kubernetes cluster. In our example, the Dockerfile can be found on branch ssr, which can be built and started with the following commands:

docker build . -t my-next-js-app:1.0.0
docker run -p 3000:3000 my-next-js-app:1.0.0

Our application can then be called up as usual under localhost:3000.

Dockerfile is inspired by https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile

Conclusion

NextJS promises big things with their slogan The React Framework for Production! In fact, NextJS offers numerous advantages compared to a normal React application, which are especially important in production.

As with many frameworks and technologies, you have to be clear about your use cases and the area of application beforehand. In addition to the frontend-facing functions, NextJS also offers a wide range of options such as connecting to a database. Here you have to decide, based on the size and complexity of the application, whether using another technology, e.g. Spring Boot with Hibernate, could be more suitable.

Nevertheless, NextJS offers a very good developer experience with numerous modern functions for small to medium-sized web applications and especially interactive blogs, which have a positive effect on the user experience.

As always, the examples can be found on GitHub.

Stay tuned! 🚀


This text was automatically translated with our golang markdown translator.

Tom Trapp - problem solver, innovator, athlete. Tom prefers to work on modern software all day long and attaches great importance to objectively clean, lean code.