7 Frontend Architecture Lessons From Nuxt.js
Front-end architecture is a hot topic, and for good reason.
As more and more logic has moved into JavaScript on the front-end, applications have become cumbersome to deal with.
This has in turn led to the rise of frameworks and application patterns to manage that complexity.
While there is no single right choice to architecture, there are a number of patterns that have begun to emerge.
These are often encapsulated in starter templates or even full-on application frameworks.
One such application framework is Nuxt.js. Nuxt.js provides a higher-level application framework on top of Vue.js.
By using Nuxt.js, you opt in to a particular set of front-end architectural decisions. In this post, I'm going to go through 7 of those decisions and draw out some of the advantages of each.
At the end, whether you choose to use Nuxt.js or not, you can draw from these lessons for building your own applications.
1. Universal JavaScript with Server Side Rendering and Hydration
One choice that Nuxt.js makes for you is that your application should be able to render the same pages and components on the server and the client.
Server-rendered pages deliver a full page of HTML instantly ready for rendering, and then "hydrate" the page with JavaScript on the client to add interactivity and turn the page into a Single Page Application (SPA) for future navigation.
This means that Nuxt applications are separated JavaScript applications that load data using an API interface, not embedded in another server-side application frameworks or layered onto static HTML.
It also means that logic that can only run on server (such as filesystem access) or client (such as libraries accessing window
) must be contained within special lifecycle hooks or the special 'no-ssr' wrapper component.
The benefit is that you gain some of the best properties of both server-side rendered and SPA applications - a fast time to first page view, while also being network efficient and having all of the latency and interactivity benefits if a SPA.
2. Prefetching Asynchronous Data
In order to realize the benefits of pre-rendering pages on the server, you need to make certain that your server-side rendering has all of the data it needs before rendering. This is trivial for static pages, but for dynamic applications that depend on API calls, you will need to make sure all critical data is fetched before the page is rendered and sent from the server.
Even for pure SPA applications, it can be helpful to have hooks that let you specify what data is necessary before a page is rendered, and what can be filled in afterwards.
In Nuxt, there are three distinct hooks provided specifically for this purpose: nuxtServerInit
, fetch
, and asyncData
. It is also possible to use middleware for this purpose.
Each of these hooks has different nuances and usecases, but the architectural pattern is clear: Provide mechanisms for prefetching any type of asynchronous data used in your application.
3. Deconstruct pages into Layouts, Pages, and Components
One of the beautiful things about component-based architectures is that one can conceive of everything as a component.
However, when translating that into a system that uses routing to create distinct pages, it is useful to add some more structure on top of that abstraction.
Nuxt does this by using the concepts of pages and layouts. A page corresponds to a route, and fits naturally with how we're used to thinking about the web. Each page can have a layout that it renders within, so layouts become a way of creating shared structure across pages.
These tools - pages and layouts - not only can be used to share template structure, but provide natural hooks for middleware or data prefetching. For example, an admin layout might not only show the admin navigation but also include middleware that checks that a user has admin permissions, redirecting if not.
4. Organize File Structure by Role
One of the first questions in any sort of application is how to organize the files. Nuxt takes a relatively simple approach while still creating structure.
Files are separated by role, with directories for components
, layouts
, pages
, middleware
, plugins
, the store
, compilable assets
, and completely static
assets.
I have found it also useful to add directories for mixins
and lib
(aka other unassociated logic like API wrappers), but your mileage may vary.
5. Filesystem Based Routing
Another architectural pattern that Nuxt introduces is filesystem based routing. While less flexible than purely programatic routing, this approach brings a number of advantages.
First and foremost, it makes it easy for newcomers to the codebase to find exactly where to start when looking at a particular page. Trying to debug the /login
page? Take a look at pages/login.vue
.
It also reduces the number of decisions you need to make by standardizing a route structure.
Most ideal for more static routes, the segment-based dynamic routing provides enough flexibility for most websites, and if you absolutely must have more complex routes you can always fall back to a completely custom router.
6. Decompose Your Vuex Store Into Modules
Vuex provides the ability to split your store into modules
, with (optionally namespaced) separate state objects, getters, mutations, and actions.
In Nuxt, this mode is recommended and default, with individual files in the store
directory automatically becoming namespaced Vuex modules.
While for simple applications it might be overkill, this is almost a pure win. Especially since Nuxt takes care of all the boilerplate of setting up a module-based store without you needing to worry about it.
7. Use Middleware To Share Logic Across Routes
Middleware is a very common approach to sharing functionality on the server side, allowing developers to layer on functionality that checks a request, performs some logic, and either adds some data or decides if a redirect is necessary.
Nuxt takes that concept and applies it to client-side routing as well. And because of the Universal JavaScript architecture, it sets things up so the same middleware runs on whichever of server or client happens to be rendering the page.
Middleware can be applied at either the layout
or the page
level, making it ideal for things like checking authentication or authorization.
It also can be run in an async mode, letting it prefetch data similar to other async hooks. The only disadvantage of using it for this purpose is that later middleware can still result in a redirect, so expensive API calls should probably be saved for later in the lifecycle.
Wrapping Up
As we've covered, Nuxt makes a number of front-end architecture choices for you right out of the box.
This can dramatically speed up the process of building a complex Vue project.
They also make server side rendering a breeze, doing all of the configuration you might otherwise need to hand-tune.
For those who don't want to go all the way to Universal JavaScript, they also provide a pure SPA build mode, and for those with static content you can also prebuild every page statically similar to how GatsbyJS or Vuepress work.
I highly recommend trying Nuxt for your next Vue project, but even if you don't there is plenty to learn from the way they approach front-end architecture.