Routing Modes

A unique feature of One is the ability to seamlessly route across a variety of route modes. What this means in plain english, is that you can choose whether each individual page within your routes uses one of a few strategies, such as rendering statically at build time, or rendering on-demand at request time.

One supports the following routing modes, which are explain in detail below: api, spa, ssr, and ssg.

You can set your global routing mode with the One vite plugin. The default is ssg.

vite.config.ts

import { one } from 'one/vite'
export default {
plugins: [
one({
web: {
defaultRenderMode: 'spa'
}
})
]
}

To specify the routing mode on a per-page basis, you add a suffix to the filename, like so:

  • route+ssg.tsx - Matches "/route", but will render the page as a SSG route.
  • route+spa.tsx - Matches "/route", but will render the page as a SPA route.
  • route+ssr.tsx - Matches "/route", but will render the page as a SSR route.
  • route+api.tsx - Matches "/route", but will render the page as an API route.

Modes

Note that render modes mostly only apply to the web. On native, everything is essentially a SPA, with all the JS for every route included in the app the user downloads from the app store. Because native apps don't have to deal with the network, this SPA mode is ideal and has very little downsides.

SSG (the default)

When you don't add a + suffix to your route, it will default to an "SSG" route, unless you set the web.defaultRenderMode configuration otherwise. An example of a SSG route would be something like app/blog.tsx if you haven't changed the defaultRenderMode, or app/blog+ssg.tsx if you have.

SSG Stands for "Server Side Generated", it's a strategy for web where you pre-render the entire HTML and CSS of a page at build-time fully on the server (or in CI) so that you can serve the page quickly from something like a CDN, without running any JavaScript at the time of request.

We chose SSG as the default behavior as it serves as a good balance between simplicity and performance. What it is less good at is serving dynamic content to an individual user.

That said - you can still do dynamic content on top of an SSG page. It's just that you'll have to have the dynamic content load in after the browser finishes downloading and parsing the JS for that page, and it will need to gracefully replace the static content that the user sees.

This pattern is great for things like a SaaS homepage that shows mostly static content, but where you may want to load a logged-in menu for the current user. It's not good for something like a logged-in dashboard.

SPA

If you name a route with the +spa suffix, like app/dashboard+spa.tsx, it will no longer render on the server - either at build-time or at request. Instead, at build time One will build just the client-side JavaScript necessary.

This render mode is great for highly dynamic use cases. Think a Linear, or a Figma, a metrics dashboard, or a user account panel. It is simpler to build, and doesn't require making sure every dependency works on both Node.js and in the browser.

It's downsides include a slower initial load time, and worse SEO. Though we are working on allowing _layout.tsx files to also leverage render modes, in which case you could have a really nice hybrid mode where you render layouts statically or on the server, but keep the "meat" of the page dynamic. This would retain some upsides of SSR/SSG, while simplifying things quite a bit.

SSR

If you name a route with the +ssr suffix, like app/issues+ssr.tsx, you will get a server rendered page. This mode will generate the JS for both server and client at build-time, but instead of rendering out the HTML and CSS right then, it will instead wait for a request to the production server before it imports the server JS, renders the page, and then returns the HTML and CSS.

This mode is great for things that need to be dynamically rendered more often than whenever you deploy. One example is something more like GitHub Issues. By server rendering a page like this, you get faster initial loads, and better SEO than a SPA-type page. But it is the most expensive in terms of cost - each request will now run a full render of the route, and also slower than SSG in terms of initial response time, at least by default.

In the case of a GitHub Issues type page, what you'd do is cache the SSR response on a CDN, and then clear the CDN cache whenever data for that page updates. This is complex, and generally SSR is the most complex of the three render modes to support. Because it is more complex and more expensive, we generally recommend using SPA or SSG unless you are certain you can afford to pay for the extra render cost, and/or cache responses without too much trouble.

API

Adding +api.tsx to the end of your filename turns a route into an API route. This is a special type of route that is different from the other types in that it expect to be used as an API endpoint that you call from other routes or services, and not as a place an end-user would navigate to directly via their browser.

API routes have a few standard exports. The default export acts as a catch-all:

export default (request: Request): Response => {
return new Response.json({
hello: 'world'
})
}

We've aligned API routes with the Web standard Request and Response objects. This ensures they will be easy to use, well documented, and have broad compatibility with web servers. Note that TypeScript and Node >= 20 ships by default with global Request and Response objects, so you don't need to import them at all.

If you define a ./app/test+api.tsx page with the above code, you can curl https://localhost:3000/test and you will receive a response with Content-Type set to application/json, and the contents of {"hello":"world"}.

You may also export a function for each of the supported HTTP types:

export const GET = (request: Request): Response => {
return new Response.json({
hello: 'world'
})
}

One exports a type helper called Handler that you can use to make typing easier:

import { Handler } from 'one'
// this route will be typed as "(request: Request) => Response | Promise<Response>"
export const GET: Handler = (request) => {
return new Response.json({
hello: 'world'
})
}

Edit this page on GitHub.