Jamstack is a popular approach for building web apps. But it can be challenging to use the Jamstack in highly interactive apps. Learn more about how to solve those challenges with Prisma and Next.js.
Contents
- Rendering and data storage in web applications
- The server-client spectrum
- What is the Jamstack?
- Jamstack with Next.js and Prisma
- Jamstack best practices
- Conclusion
Rendering and data storage in web applications
As developers, we are often faced with decisions that will impact our applications' overall architecture. In recent years the Jamstack architecture has gained popularity in the web development ecosystem.
The Jamstack is not a single technology or set of standards. Instead, it's an attempt to give a name to widely used architectural practices for building apps that aim to deliver better performance, higher security, cheaper scaling, and better development experience.
Evaluating the suitability of the Jamstack for a project can be tricky since there are several different ways to build a web app, all involving different trade-offs about:
- Where to implement rendering and logic in the application?
- Where to store data for the application?
In this article, I'll explore the role of those decisions in choosing an architecture, diving into the drawbacks and trade-offs in Jamstack applications, finally examining a hybrid approach with Next.js and Prisma.
The server-client spectrum
Before diving deeper, it's worth understanding that the decision of where to implement rendering and logic in an application can be seen as a spectrum ranging from server rendering to client side rendering with some approaches in-between.
Architecture | Description | Rendering Logic | Rendering time |
---|---|---|---|
Server-rendered application (monolith) | Server which renders HTML per request and queries a database render templates. Examples of this approach include Wordpress and Rails apps. | Server | Per Request |
Static site/Jamstack | Pre-rendered HTML that is served by a CDN to a browser. Traditionally, this HTML was just manually written in .html files. Nowadays, static site generators are often used to generate the HTML. Some static sites enhance of the statically generated HTML with JavaScript for more granular interactivity. | Build process | When source/content changes |
Single page application (SPA) | An application written in JavaScript where the web server sends an empty HTML page along with the JavaScript application to the browser. The browser then executes the JavaScript code which in turn generates HTML. These are typically used in interaction heavy apps where page reloads are avoided. | Client side | Per user interaction |
The main difference between the three approaches is where the rendering logic is implemented and when is content rendered. With the Jamstack approach to rendering happens per change in the content which causes the build tool to trigger a new build.
What is the Jamstack?
Jamstack is a broad term referring to an architecture that uses client-side JavaScript, reusable APIs, and prebuilt markup to build websites and apps. The term Jamstack was coined by Mathias Biilmann, the co-founder of Netlify, a cloud platform targeted at serverless functions and static websites.
At its core, the Jamstack says that a web app can be rendered into static HTML files (aka markup) at build time, and the HTML files are then efficiently served to clients via CDNs (content delivery network).
Static generation for performant and SEO friendly sites
Building a Jamstack site is typically achieved with static site generators such as Next.js, Gatsby, Nuxt, and Hugo. Static site generators use markdown or source data from APIs during the build, render the markup, and upload the static files to a CDN.
Jamstack sites are generally fast and responsive because they have all their pages pre-rendered, and CDNs can serve high loads. This approach results in performant and SEO friendly sites while avoiding the burden of authoring HTML manually. For these reasons, Jamstack is a popular choice for marketing sites and blogs, which typically have little interactivity.
Decoupling the client and server code
The JAMstack pushes for decoupling your client (frontend) and server code, where the server code exposes an API (GraphQL or REST). The API is then used to statically generate the app during to build and augment the pre-rendered app with additional client-side functionality. The API can be deployed as a serverless function to AWS or as a full-fledged server. You can build a custom API or use one of the many third-party APIs:
- Content management with a headless CMS (content management system) such as Contentful, Strapi, and GraphCMS.
- Ecommerce with BigCommerce and Snipcart.
- Identity and authentication with Auth0.
- Payments with Stripe
- Search with Algolia.
All these APIs are, in essence, a data storage mechanism. For example, Auth0 stores user accounts and credentials for you; Snipcatcart tracks your users' shopping carts; Contentful stores your content. The benefit of such APIs is that they also give you a UI to manage your data. For example, content creators can publish with Contentful using the UI without ever touching the blog's source code.
Alternatively, you can build your own API and implement the logic specific to the feature you're implementing and use a database for persistent storage. This can be done by deploying the API to a serverless function or using a microservices architecture.
Rendering immediate user interactions
Either way, because Jamstack apps typically require the app to be rebuilt for every state change, rendering immediate user interactions is one of the most common challenges. For example, in a blog that allows readers to comments on posts, every time a reader comments on a post, the comment must be persisted followed by a rebuild of the app to render the comment.
Generally, you can implement commenting functionality in the following ways:
- Build an API (or use a third-party API) to store comment submissions and trigger a build for every submission to re-generates with the comment. This approach works on a small scale; however, if every interaction triggers a build, it can get out of hand, mainly if builds take too long.
- Delegate immediate interactions to the client-side and avoid rendering comments when statically generating the blog; for example, embedding Disqus uses this approach where the blog post's content is primarily pre-generated, and comments are fetched and rendered in the frontend. But this defeats the goal of pre-rendering because, after every page load, a client-side request is made to Disqus to fetch and render comments.
While comments may seem like a trivial feature, they emphasize the challenge of reflecting immediate interactions in statically generated Jamstack apps (and have been shared by others). Thus, introducing more interactivity to Jamstack apps requires reducing the time cost of builds.
In the next section, we'll see how Next.js solves this problem
Jamstack with Next.js and Prisma
Next.js is a react based framework that supports hybrid static and server rendering. It is well suited for Jamstack because it allows you to define how each page in your app is rendered. Next.js solves the problem of time costly full rebuilds that Jamstack apps are prone to, especially when building an interactive app where user interactions should be reflected quickly.
Prisma is an open source ORM that makes working with a database easy. Together with Next.js they form a powerful toolset for building dynamic Jamstack apps that are backed by a database. You can use Prisma to access the database at build time (getStaticProps
), at request time (getServerSideProps
), or using API routes which you can use to expose a REST or GraphQL API.
In Next.js, the main building block is page. A page is a React Component exported from the pages
directory and each page is associated with a route based on its file name, e.g. the pages/about.js
corresponds to the /about
route.
Rendering forms in Next.js
Next.js supports two forms pre-rendering: static generation and server-side rendering. The difference is in when it generates the HTML for a page.
- Static generation: The HTML is generated at build time and will be reused on each request. Data for static generation is fetched in the
getStaticProps
function exported by the page. - Server-side rendering: The HTML is generated on each request. Data for server-side rendering is fetched in the
getServerSideProps
function exported by the page.
Note: Server-side rendering deviates from Jamstack because content is rendered per request.
Next.js lets you choose which pre-rendering form you'd like to use for each page. You can create a "hybrid" Next.js app using static generation for most pages and using server-side rendering for others. Additionally, statically generated pages can be configured to be re-generated at run-time.
Incremental static re-generation
Incremental static re-generation (ISR) allows you to re-generate statically generated pages as traffic comes in. You can enable it per page. That means that when the first request comes in after the site was built, the already generated version is served, while in the background, the page is re-generated. Once the re-generation completes, the re-generated version is served. To enable it, you set the number of seconds to wait from the moment a request comes for the re-generation to happen in the getStaticProps
function of the page.
Note: This feature is currently only available when deploying Next.js to Vercel.
Incremental static re-generation gives you the performance and scalability benefits of static generation by reducing database and backend load while allowing dynamic content.
If you're coming from server-side rendering, incremental site re-generation is like server-side caching with Varnish. The main difference with Next.js is that the server caching rules are controlled directly in the application code rather than in a separate caching component in your infrastructure.
Blog with comments example
Suppose you were building a blog that allows readers to comment implementing it with Next.js and Prisma. The database will have two tables: Post
and Comment
with a 1-to-many relation between Post
and Comment
to allow multiple comments per blog post.
Pages
The blog will need two main pages:
- Blog page which lists the recent blog posts - Route:
/
- Data requirements: Blog posts - Rendering: should be statically generated with incremental static-re-genration to re-generate the page at most once a second. Creating a new post will take around a second to be reflected. - Post pages which render a single blog post and related comments. - Route:
/post/[id]
where[id]
is a post'sid
from the database - Data requirements: The blog post and associated comments - Rendering: Existing blog posts should be statically generated with incremental static-regenration so that new comments are reflected.
Blog page
Let's look at the getStaticProps
function for the root page of the blog:
The getStaticProps
function handles data fetching and the incremental re-generation configuration with the revalidate
key in the returned object. By setting it to 1 second, we ensure that if a new blog post is published, it will take around 1 second + build time for the page
at most until it's rendered.
Now let's look at the corresponding component for the blog page:
In the example above, the Home
component gets the posts
from the props from getStaticProps
and renders them.
Post pages
Now, let's look at the second page for individual posts and comments. Because the page's route is dynamic, i.e. containing the id
parameter /post/[id]
, we define two data fetching functions:
getStaticPaths
: Responsible for fetching all existing blog post ` id's from the database to generate their respective pages.getStaticProps
: Responsible for fetching the data for a given post. This function will run once for every postid
returned by thegetStaticPaths
function.
Now let's take a look at the code:
There are two important things to notice here with regards to incremental static re-generation:
fallback
is set to true ingetStaticPaths
so that if a request is made to a blog post that wasn't available during the build, Next.js will incrementally generate that at run-time when the post is first requested.revalidate
is set to one second ingetStaticProps
so that if a new comment is left on a post, it will take at most 1 second for the page to be re-generated with the new comment.
Demo
To get a sense of how this works, here's a demo based on the code above showing the root page of the blog. The source code can be found on Github.
In the gif below:
- A new post is made by making a request to the API, which creates a new post in the database.
- After the post has been created, the page is reloaded twice
- The first reload triggers an incremental re-generation in the background while returning the stale version.
- The second reload loads the re-generated page with the new post.
To further emphasize the difference between the rendering approaches in Next.js, check out the post pages in the demo. The post page has two variants, one uses incremental static re-generation (accessible via the Open static button) and the other server-side rendering (accessible via the Open SSR button). As you can imagine, the static one loads much faster.
Here's a quick lighthouse performance comparison of the two variants: Server-side rendering:
Static generation:
Jamstack best practices
To unlock Jamstack's benefits, consider the following best practices when using Next.js and Prisma.
Serving everything from a CDN
A content delivery network (CDN) refers to geographically distributed servers that work together to ensure fast delivery of files such as JavaScript, HTML, CSS, and images. By distributing assets closer to website visitors using a nearby CDN server, visitors experience faster page loading times. Additionally, you reduce the overhead of maintaining a server.
When deploying a Next.js app to Vercel, all assets are automatically served from a CDN except server-side rendered pages. As demonstrated in the example above, you can avoid server-side rendering by using static generation along with incremental static re-generation. That way, you can the best of both worlds – dynamic content without losing out on the benefits of serving from a CDN.
Deploying the database and serverless functions to the same region
You can reduce the response times of serverless functions that use Prisma by following two guiding principles:
- Deploy the functions and database to a region close to most of your users to improve response times for client-side requests to the API.
- Deploy the database to the same region as the functions to reduce the latency of incremental re-regeneration and run duration of functions.
These principles are based on the multiple round-trips that take place when a user makes a request a serverless function:
- Between the user and the serverless function.
- Between the data centers of the serverless function and database to initiate the database connection and for every query.
With Vercel, you can choose the deployment region of the serverless functions using the vercel.json
configuration file.
When choosing the regions, you can refer to the following article for latency measurements between regions.
Prefetch links for instant page transitions
In Next.js, navigation links are implemented with the <Link>
component. The <Link>
component will prefetch pages by default if the link is in the viewport (visible to the user). With prefetching, page transitions are instantaneous as data is prefetched, and the rendering happens in the frontend without a page reload.
To read more about prefetching, check out this article.
Automated builds
Jamstack apps rely on static generation for the markup to be built. By default, commits to the Git repository of your app will trigger a build with Vercel. However, this doesn't necessarily include changes in the database.
In an app built with Next.js and Prisma, incremental static re-generation builds pages that use the database in the background as requests come in. In other words, you don't need to trigger a build for every change in the database to be rendered.
Conclusion
In summary, Jamstack is a loosely defined term to describe architectural practices for building more performant apps with stronger security, cheaper scaling, and a better development experience. Typically this is achieved by using static site generators and serving the pre-rendered HTML from a CDN.
While the Jamstack is very suitable for content-heavy sites, it can be challenging to adopt in highly interactive apps where user interactions should be quickly reflected. Hence, thinking about where you implement rendering and logic and store data can help in determining the suitability of Jamstack for what you're building.
Next.js has a hybrid rendering model that allows you to choose between server-side rendering and static generation. It gives you the flexibility to make that decision based on the needs of the given page. And incremental static re-generation solves the problem of time costly rebuilds common to Jamstack apps.
Prisma is a next-generation ORM that makes working with database access easy. It integrates smoothly with Next.js and can be used to fetch data for static generation and server-side rendering and build API routes.
Adopting the Jamstack architecture often involves composing frameworks, cloud services, and third-party APIs, resulting in a completely new development workflow with its own set of trade-offs and operational concerns. Therefore, it's useful to have a solid understanding of differences in rendering techniques and the role of data and interactivity in your app. That way, you can make an informed decision.
To learn more about Prisma and Next.js, check out the Prisma docs, and the Next.js docs.
Don’t miss the next post!
Sign up for the Prisma Newsletter