<onWebFocus />

Knowledge is only real when shared.

tRPC, HTML & CSS and Serverless Databases

November 2022

An introduction to tRPC and a discussion on the relevance of HTML & CSS.

tRPC for Typesafe Interfaces

When designing a data interface that uses JSON, you typically have two options: a classic REST interface or a GraphQL interface. A REST interface is straightforward to implement using a framework like express, but a GraphQL interface offers greater flexibility and can easily serve multiple clients.

Neither of these interfaces (REST or GraphQL) typically allow for typesafe integration into the client application. It is possible to generate types from a GraphQL schema, but it can be difficult to do so. The goal of @trpc/server is to offer seamless and typesafe integration of backend routes into the client. The RPC in the name stands for Remote Procedure Call, which means that the idea is to make it easy to call functions through an HTTP request in the background, similar to calling a function directly.

This idea originated in the Next.js community, where the backend and frontend are closely integrated and often written by the same person. In this environment, typesafety is particularly appealing because any issues that arise at the interface when adding features or refactoring will be immediately detected by the type checker. With @trpc/server, the client accesses the types directly from the source code, rather than a generated version, which means that no additional build is required and changes are reflected in the client immediately. This also means that the client will hot-reload whenever any associated data source changes, so it always displays the current state.

Basic Setup

The purpose of this post is not to provide detailed usage instructions, but rather to discuss when, where, and whether tRPC might be useful. Below is an example of a simple router (on the server-side) with two queries and a mutation procedure. The inputs are specified using the zod library, which enables the type checker to warn when the data in the client doesn't match the expected types.

import { initTRPC } from '@trpc/server'
import { z } from 'zod'
 
const t = initTRPC.create()
const users: [{ id: 1, name: 'This' }, { id: 2, name: 'That' }]

const appRouter = t.router({
  listUsers: t.procedure.query(() => users),
  userById: t.procedure
    // Using zod to validate inputs.
    .input(z.number())
    .query(({ input }) => {
      return users.find(user => user.id === input)
    }),
  addUser: t.procedure
    .input(z.object({ name: z.string() }))
    .mutation({ input } => {
      const user = { id: users[users.length - 1].id + 1, name: input.name }
      users.push(user)
      return user
    })
})

// Interface types to be imported by the client.
export type AppRouter = typeof appRouter

Using @trpc/client on the client side, you don't need to manually perform any fetch requests. The library handles this for you.

import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from '../server'

// Pass in the AppRouter types from the backend as a generic parameter.
const trpc = createTRPCProxyClient<AppRouter>({
  links: [ httpBatchLink({ url: 'http://localhost:3000/api/trpc' }) ]
})

const allUsers = const user = await trpc.listUsers.query()
const specificUser = await trpc.userById.query(1)
const newUser = await trpc.addUser.mutate({ name: 'Then' })

When using Next.js, the entire router can be exported from a single Serverless function. To do this, place the server in the /pages/api/trpc/[trpc].ts file using a special wildcard syntax. This will allow the URL to match the example given above. Note that using a single Serverless function deviates from the standard Next.js build, which generates a separate function for each page and API route. As a result, it's important to keep the router relatively small and to use other regular Serverless functions for additional functionality.

tRPC is often used in conjunction with a set of React Hooks provided by @trpc/react-query. These hooks handle caching and data invalidation, and can be used to replace a separate state management library like valtio or mobx. It's worth noting that tRPC relies on a stable connection and can introduce significant latency when the state is managed through the server, so it may not be suitable for interactive applications.

On the server side, tRPC does not provide any assistance when working with data. You will often need to read and write data from a database, and tools like prisma can be helpful in this regard. Prisma allows you to access the database content in a typesafe way by first specifying a schema.

HTML and CSS

Recently, there has been a resurgence of backend frameworks that focus on rendering HTML and CSS on the backend. These approaches recognize that a lot of JavaScript running on the client to render markup and CSS is often unnecessary, and that client-side frontend frameworks may have been overused.

To understand whether this trend is moving in the right direction, it's helpful to examine the history of HTML and CSS in web browsers. Most traditional computer applications don't use markup to render their user interfaces; they do it programmatically. However, in the early days of the internet, connections were slow, so browsers were designed to transfer as little data as possible while displaying websites. Additionally, it was important that anyone could create a website using this transferred format without any intermediate steps.

In the early days of web browsers, websites were rendered using only HTML and CSS. These technologies allowed for the creation of static websites, as there was no way to add interactivity programmatically. Any interactivity on a website required loading a new page. HTML and CSS are used to describe and transfer a user interface, and are rendered by the browser. JavaScript was later introduced, which allowed for small interactive elements on a webpage. Today, frontend frameworks running in JavaScript are used to render HTML and CSS dynamically.

Would a browser written today include HTML and CSS? The answer is no. JavaScript and JSX (HTML in JavaScript) already exist and provide everything needed to render the UI. For example, React Native uses a JavaScript engine to render whole apps in React, which instructs the native UI on what to display. Data for dynamic content can be loaded as JSON, and apps can even download additional code for rendering. Inline styles, which are easy for developers to use, are sufficient for programmatic rendering, and there is no need for the abstraction of a class, which is only necessary when used with HTML to save size. React Native has it's own layout engine with Yoga Layout, which lacks many of the older box layout features found in every browser due to the requirement of backward compatibility.

Discounting the initial size of a framework like React that needs to be downloaded, transferring the code and data to generate markup on the client often requires less bandwidth. This is because static markup cannot account for repeating elements as it lacks control structures like loops. However, in some cases it may be more efficient to transfer the resulting markup, such as when using a library with many features, like a syntax highlighter. In these situations, React Server Components can be used to render certain parts on the server, without transferring plugin code when there is no need for interactivity on the client.

💡Hear Me Out💡 Serverless Database on the Edge

Currently, there aren't many good options for serverless databases. Vercel, one of the most popular hosting providers, does not yet offer a database. Most recent updates from Vercel in next have focused on rendering content as close to the user as possible, on the "edge". Vercel recently announced Edge Config, which allows the deployment of a small amount of read-only configuration data to any edge location. This read-only data resembles a database in theory, but it is its read-only nature that allows it to be deployed to the edge.

Databases are generally not suitable for use on the edge, as they require a single source of truth, which can only be achieved when the data is stored in a single location. However, it is possible to distribute a database to the edge without negatively affecting performance. This could be done by leveraging the common practice of requiring users to log in to access dynamic functionality in interactive applications.

Many applications display data that is specific to the current user. For example, a bookmark service only stores and displays the bookmarks for the user who is logged in. This central feature can be leveraged to create a database that is present on every edge, but only contains data at the edge where it is accessed by the user. This leads to very fast lookups, as most people usually stay in the same location for a long time. If a user switches locations, the performance penalty of moving their data to another edge will be tolerable compared to the benefits it provides. It may also be possible to make data either shared or user-specific, where access to shared data would always involve a request to a central location.

This post was revised with ChatGPT a Large Language Model.