All in one: SPA, SSR, SSG and API
Recently, I started to work on feez.ws, a new app that allows tracking progress in public, giving you a platform to share your journey with friends, family, and a community of like-minded individuals.
I am also the founder of Stormkit, so this was a perfect case for me to dogfood my product and host Feez on Stormkit.
In this blog post, I'll write about how I used Vite with React to create a monorepo capable of server-side rendering, generating static pages and, at the same time, acting as a single page app.
If you're curious and would like to see the end result immediately, here's the repository: Monorepo Template.
How to structure the repository
Before starting to work on the application, I roughly had the following structure in mind:
|feez.ws||Static page (SSG)||Home page, terms, pricing, etc... all static content.||Yes|
|feez.ws/my||Single page app (SPA)||Authenticated part of the application.||No|
|feez.ws/:name||Dynamic page (SSR)||User-generated content.||Yes|
Usually, it is a common practice to have two different repositories for the landing page and the application. However, with Feez, I wanted to keep things simple and manage only one code base. The following image illustrates what I wanted to achieve:
Why not use a framework?
As a matter of fact, I love using React, but I prefer not to use frameworks for a few reasons:
- They change frequently, and it's hard to keep them up-to-date.
- Every other day, there is a new framework in the space
- The learning curve
Since I started experimenting with Vite, I found a built-in way to make everything these frameworks are doing, which I needed for my App: server-side rendering (SSR), static site generator (SSG), single-page application (SPA), and an application programming interface (API).
How does Server side rendering work?
Let's have a look at the following chart. It explains the server-side rendering logic:
The flow that the image describes is as follows:
- User makes a request
- Our Node Server receives the request and serves the entry file - see code. At this stage we:
- Serve the response to the browser - see code
- HTML document loads our client-side application - see code
- The client-side application hydrates on top of the server-side rendered page - see code
- Hydration means how React “attaches” to existing HTML that was already rendered by React in a server environment.
- After the hydration is complete, the App is reactive.
How to generate static pages?
This step is simply another little addition to the server-side rendering. All we have to do is to spawn a Node Server, ask it to render pages that we want to be static, take a "snapshot" of the response and, save it into an HTML file.
I created a file called src/prerender.ts which returns an array of routes to be generated statically. In this example it's a hard-coded string array, but it can also be a dynamically constructed array (such as the result of a directory scan).
Finally, I added a build step in package.json which calls the src/vite-server.ts file to generate static pages.
How does hydration work?
There are two things to keep in mind here:
- Use React.hydrateRoot instead of React.createRoot when mounting the App
- If your page is making an async request, inject the response into the document on the server side and retrieve it on the client side to avoid redundant calls to the API.
What about the API?
The src/vite-server.ts file contains a route for
/api endpoints. When it receives a request, it uses Stormkit's
matchPath function to determine the file that will be loaded under the
The end result
When you build the application this will create a
.stormkit dist folder (you can change this setting in the vite configuration files) and inside it you'll find three subdirectories:
If you go ahead, create a repository from the template and deploy this on Stormkit, it will upload the
public folder to the CDN and the
api folders to the lambdas. The load balancer will then load the static pages when the route exists and fallback to the server for dynamic rendering. Endpoints starting with
/api will be forwarded to the API functions. So you have a fully hybrid with a monorepository.
Here's an example: https://monorepo-template.stormkit.dev/.
Pretty neat, isn't it?
If you enjoyed this blog post, give it a star and deploy it on Stormkit 🙏🏻 And also, I'm happy to connect over Twitter.