Using Vue as server side renderer.

This blog is built with the Nuxt framework that is a Vue-based, server-first framework. The first load for any given view is rendered on the server first, and then the app is "hydrated" with the front-end Javascript packages. Now, Vue itself offers tutorials and the tools to do this and I was wondering how different it would be to build apps that way instead of using something like Nuxt.

Turns out, very different. You are basically doing two renders; on the server and on the client. The server side rendered view only lives for a blink of an eye as it's quickly replaced, making me question the benefits of doing this. The server render is mainly for search engines and web crawlers. If someone has their Javascript turned off, they're not gonna use that app anyway as it lacks all the click events etc. And if you made an web app instead of a web page, you probably have most views behind a login so the crawlers never get to see them.

What if I didn't do the hydration round at all?

The idea came to my mind that what if I used Vue like I use PHP at my job? What if I made an app that is fully Javascript, but none sent to the client? Code like it was 2004. Some projects might require you to do zero client JS, so what would you use then, JAVA?! GWT the hell outta here!

The basics

The simplest proof-of-concept app is a TODO app. I decided to leave out authorization logic so the app is only living on your computer for you. The code is on Github if you want to check it out.

So what’s the point? Why not just use something like pug or template literals and pure JS? Well, as it turns out Vue rendering is quite neat. I can still split my views into separate components and use directives like v-if or v-for, the only thing that is missing are the client side events like @click and the component lifecycle hooks. The most surprising thing was that it also runs the computed values once, so you can really leverage that in list filtering, data mapping, and so on.

The app has only three main ingredients: ExpressJS for the basis, Vue for template rendering and SQLite for the database.

Anatomy of the app

The whole app consists of only links and forms to navigate and to act. Forms with POST actions lead to endpoints that are doing their magic and then redirect the user to the next (or the same) view. If the server is fast, this feels immediate and if the server was busy our app feels very sluggish.

Here's the main view with the tasks categorized by todo and done, it has a simple search filter and every task has edit and status actions. The elements mark in red are simple links to the edit view and the elements marked with orange are forms.

The search form posts to the main view itself and the search GET parameters is used in a computed value when the tasks are filtered. The set done/undone button is a form that posts to an endpoint that sets the task's status and then redirects back to the main view.

This is the edit view that is also used to create new tasks depending if the id in the GET parameter is a number or not. Again here the red marked "Go back" is just a link to the main view. The form marked in green is the edit form itself, and if we are editing, market in orange we have a delete button for another form that handles the deletion.

Interactivity

So with no JS on the client how are we creating interactivity? Forms. We are submitting forms with the POST method, and links of course for route changes. A simple button on a TODO list item to set the item done is a <form> with hidden form fields and a submit button. The form sends the user to an endpoint that saves the data and redirects to another view.

The other way to pass app state is in the url GET parameters. This is nothing new, on the contrary, this is pure HTML as it was meant to be. The magic lies on the server though. We can wrap these button-forms as Vue components to ease the job.

The button that toggles a TODO item as done or undone:

export const TodoAction = {
    props: ['todo'],
    template: `<form action="/update" method="POST">
        <input type="hidden" name="todoId" :value="todo.todoId" />
        <input type="hidden" name="title" :value="todo.title" />
        <input type="hidden" name="description" :value="todo.description" />
        <input type="hidden" name="state" :value="todo.state ? 0 : 1" />
        <input type="submit" :value="actionLabel" />
    </form>`,
    computed: {
        actionLabel() {
            const { state } = this.todo

            return state ? 'Set undone' : 'Done'
        },
    },
}

Now compare having to repeat those forms across your app to this:

<TodoAction :todo="todo" />

Building a larger application with only pure HTML would be a challenge, but with Vue, even server-side only, it becomes much easier. Having self-contained components on server side is great.

Conclusion

I wrote the actual code for this in the summer of 2024 and then spent a good 8 months trying to get this blog done. So writing this app was definately easier than telling about it.

All said and done, coding like this wasn't even half bad and the stack is super light. But I think it's quite obvious that when we need more complex and bigger things, we can't really work like this.

Don’t do this in production. Use Nuxt, or just pure Vue as a PWA is fine.

Have something to say on this topic? Here's my Bluesky: @opiispanen