CP Lepage

I like web development

Building this blog site with typescript-rpc

Nowadays, the line between backend and frontend is just getting blurrier and blurrier. In my latest experiments, where I started building typescript-rpc, I realized that with a bit of TypeScript magic, you can almost completely remove the separation between a client and a server. So I decided to make my own attempt of an end-to-end type safe framework. Here's how I ended up use it to make a blog site.

The main idea of RPC-ing, is to declare a set of functions that will run on the server, but can be called from a client.

// Backend, server-side
const MyMethods = {
    foo(){ 
         // some operation ...
         return { data: "foo" }
     },
    // more methods here ...
}


// Frontend, client-side
const client = initClient<typeof MyMethods>()

const query = client.foo();

In this snippet, it feels like you're simply calling the method foo, but somewhere in between the frontend-backend blurriness, it is transformed into an http fetch call. So the .foo() becomes something like :

fetch("/foo")

Knowing this, I began to wonder what if with my URL bar I go the http://localhost:3000/foo ?
What if the server RPC-able methods were at known endpoints?

I then realized that instead of only sending JSON data intended to hydrate a page, I could directly send HTML code.

const MyHTML {
    foo(): {
        const data = // get some interesting data

        // transform directly that data into your rendered HTML
        return data.map(item => 
            `<div>Some nice HTML</div>`).join("");
    }
}

This way, instead of having a complicated frontend router that figures out what to render at the path /foo, then fetches some data and hydrate the page, you can just send a rendered page at /foo (like in the good ol’ days).

So I designed typescript-rpc to take a plain javascript (TS) object and convert the keys into the path components. Like that, every method is at a predictable path.

const site = {
    // this represents "/"
    "": () => `<div>Home</div>`,

    // this represents "/about"
    about: () => `<div>About</div>`,

    // this represents "/posts/first"
    posts: { first: () => `<div>First Post</div>` }
}

Now on a larger scale, I start by creating articles with a very lite CMS and on every update I refresh my methods tree which basically rebuilds the whole site.

// defined the model of an Article
type Articles = {
    id: string,
    title: string,
    slug: string,
    date: string,
    published: boolean,
    short: string,
    content: string
}

// a function to render the HTML of an article
function page(article: Article){ return `<div>My HTML Article Render</div>` }

// function to reload the site tree
let handler;
async function reloadArticles(){
    const PublishedArticles: Articles[] = await getArticlesFromCMS();
    const site = {
        // the homepage at /
        "": () => {
            return `<div>My HTML Home Page</div>`;
        },

        // a webhook where my CMS can hit to reload the pages
        // this will be at /webhook
        webhook: () => reloadArticles()
    }

    // bind every slug to a method
    // return a rendered page with the article
    PublishedArticles.forEach(article => 
        site[article.slug] = () => page(article))

    // update the handler
    handler = createHandler(site);
}

// initial articles load
await reloadArticles();

// http server
http.createServer(handler).listen();

And this is how I built a server side rendered (SSR) site with little to no dependencies.

What I love the most about this approach, is that you can create a complete site in TypeScript, very similarly to when we are doing everything on the frontend, but now every pages are static, they responds fully rendered (the SEO says thank you) and you don't have any hydration mayhem to manage.

Yay!

There are already many projects and people working on the same approach, but keeping everything lean and agnostic is my ultimate goal.

Try a demo here

Open in StackBlitz