Nuxt SSR: transfer Typescript class instances
Tags: nuxt, ssr, vue, typescript
Published: 2021-05-14 13:30:01

Hey there! 👋

**In this article, I will teach you how to transfer class instances through _Server Side Rendering_ in Nuxt.**

You may have tried to do use class instances yourself and faced some unexpected behaviour in your Nuxt app 😖?

```typescript
asyncData (context) {
  return {
    todo: new Todo()
  }
}
```

![serialise_error_pojo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xgbqd569dehaxpfnijfo.png)

After developing a solution for myself, **I released it as a Nuxt module**. Check it out: [nuxt-ssr-class-serialiser](https://github.com/Kapcash/nuxt-ssr-class-serialiser).  
Be sure to give me some feedbacks, it's my first one!

The purpose of this article is to explain this module logic.

> _This article isn't really beginner friendly.
 You might understand it without the following knowledge, but I can only recommend you to document yourself about those topics_:
- Nuxt basis: [Nuxt documentation][nuxt-doc]
- Typescript decorators: [logrocket guide to TS decorators][ts-decorators]
- Server Side Rendering (SSR): [vuestorefront guide to vue SSR][nuxt-ssr]
- Vue class components: [vue-class-component documentation][vue-class]

[nuxt-doc]: https://nuxtjs.org/docs/2.x/get-started/directory-structure
[ts-decorators]: https://blog.logrocket.com/a-practical-guide-to-typescript-decorators/
[nuxt-ssr]: https://blog.vuestorefront.io/from-vue-to-nuxt-server-side-rendering-in-a-nutshell/
[vue-class]: https://class-component.vuejs.org

# The context

Here, I'm exposing the problem we're trying to solve:
 - Why do we need class instances?
 - And why doesn't it work out of the box?
_You can skip this section if you know what situation this is all about._

### A simple page setup

Let's say you have a page with a route "id" parameter that corresponds to a TODO entity.

```
http://localhost:3000/todos/15
```

You fetch it from an api, which returns you this object:

```javascript
{
  id: 15,
  description: "Write this article you're thinking of for weeks.",
  tags: ["Programming", "Blogging"],
  dueDate: "1987-04-20"
}
```

Now imagine you want to know if this TODO has expired its due date so you can show it nicely on the page.

You could write the code in the page itself like so:

```vue
<template>
  <div>
    <p>{{ todo.description }} </p>
    <span v-show="isTodoExpired">Todo is expired!</span>
    <span v-show="!isTodoExpired">Todo due date: {{ todo.dueDate }}</span>
  </div>
</template>

<script lang="ts>
export default TodoPage extends Vue {
  asyncData ({ $route }) {
     const todo = axios.get(`/api/todos/${$route.params.id}`).then(res => res.data)
     return {
        todo,
     }
  }

  get isTodoExpired (): boolean {
     const dueDate = new Date(this.todo.dueDate)
     const today = new Date()
     return dueDate < today
  }
}
</script>
```

The result you get:

![Todo is expired](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdfo04c3nmrgkg991xwu.png) 

And the code it totally fine!

But imagine you have to use this piece of code at different places in your app?

For me, the cleanest way to do is to create a **class instance**. This way, for every _todo_ entity, you'll be able to know if it's expired or not directly from the object.

```typescript
export class Todo {
  id: number;
  description: string;
  tags: Array<string>;
  dueDate: string;

  constructor(description: string, tags: Array<string>, dueDate: string) {
    this.id = Math.random() * 1000 // Create dummy id
    this.description = description
    this.tags = tags
    this.dueDate = dueDate
  }

  get isExpired (): boolean {
     const dueDate = new Date(this.dueDate)
     const today = new Date()
     return dueDate < today
  }
}

const todo = new Todo('Old todo', [], '1987-04-20')
console.log(new Todo().isExpired) // true
```

Nice! We have a Todo class that can contain every helper method attached to a todo object. We could imagine other methods to write in such as `isTagged`, `addTag` or whatever (remember this is a dummy example. Real world apps would have more complex entities to manipulate).

{% details What about converting a POJO to a class instance? %}
> Most of the time, we'll try to **convert a plain javascript object to a class instance** without having to map all its property in a constructor. For this, I use the [class-transformer](https://github.com/typestack/class-transformer) library, which can create a class instance from a javascript object.
> 
> It's used like so `const todo: Todo = plainToClass(Todo, myTodoObj)`
{% enddetails %}

### Updating the page with our new class

With this class, we can update our page:

```vue
<template>
  <div>
    <p>{{ todo.description }} </p>
    <span v-show="todo.isExpired">Todo is expired!</span>
    <span v-show="!todo.isExpired">Todo due date: {{ todo.dueDate }}</span>
  </div>
</template>

<script lang="ts>
export default TodoPage extends Vue {
  todo!: Todo // declare asyncData data to be type safe from `this`

  asyncData ({ $route }) {
     const todoObj = axios.get(`/api/todos/${$route.params.id}`).then(res => res.data)
     return {
        todo: plainToClass(Todo, todoObj), // Could be a new Todo() as well
     }
  }
}
</script>
```

You reload the page and... wait? What is it not working? It's showing the text as if the todo was not expired?

![Todo is not expired](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kvvws20vjwdhtnytht7t.png)
 
The code is totally fine here. The problem we have is about SSR.

### Why is it not working as expected?

I'll summarise what is happening in this situation.

1. You reload the page, so it's gonna be rendered by the server.
2. Nuxt runs the `asyncData` hook and fetch the todo object.
3. The `Todo` class instance is created
4. The page component is rendered.

Then, in Nuxt engine:
5. Nuxt sends the rendered page as a string containing the dehydrated HTML.
6. Nuxt sends the fetched data on server side as a stringified JSON to the client.
7. The client side get this response and set it to `window._NUXT_`
8. The app renders the HTML, loads the data from `window._NUXT_` and starts hydrating it.

So what's wrong here?

The key is "_Nuxt send the fetched data **as a stringified JSON**_". It converts the object returned by `asyncData` to JSON, to be able to send it by HTTP to the client.

But your `todo` attribute is a class instance. How do you convert that to JSON and then to a string?

You can't.

Or at least not entirely.

Actually, it can serialise it by keeping the class properties, but losing everything else (constructor, methods, getters etc.).

**So on the client side, your `todo` object isn't a class instance anymore, it's back to a plain old javascript object (aka POJO).**

# A solution

Now we understand why our code is failing. Our class instance is stringified, losing all its methods.

So, in order to get back those class methods, we need to **deserialise the POJO back to its class, i.e. create a new class instance from the object.**

> I advise you to not read diagonally from here. We all do this, but I'll try to be as straightforward as possible despite the complexity.

### 1. [Server side] Proper server serialisation

Nuxt SSR engine exposes some hooks we can use to customise it.

The hooks we are interested in are listed here: [nuxt renderer hooks](https://fr.nuxtjs.org/docs/2.x/internals-glossary/internals-renderer#hooks).

By the time I'm writing this article, this documentation isn't up to date. Some hooks of the form `render:` are deprecated and is replace by the form `vue-renderer:` (check it on the [source code][nuxt-source-hooks] directly)

[nuxt-source-hooks]: https://github.com/nuxt/nuxt.js/blob/2ec62617ced873fef97c73a6d7aa1271911ccfd5/packages/core/src/nuxt.js#L27

The goal here is to get the data from the `asyncData` lifecycle hook, and serialise it ourselves so we avoid the Nuxt warning we saw earlier ("Warn: Can't stringify non-POJO")

We can update the `nuxt.config.js` file like this:

```javascript
hooks: {
  'vue-renderer': {
    ssr: {
      context (context) {
        if (Array.isArray(context.nuxt.data)) {
          // This object contain the data fetched in asyncData
          const asyncData = context.nuxt.data[0] || {}
          // For every asyncData, we serialise it
          Object.keys(asyncData).forEach((key) => {
             // Converts the class instance to POJO
             asyncData[key] = classToPlain(asyncData[key])
          })
        }
      },
    },
  },
},
```

> The `classToPlain` method comes from the `class-transformer` library

This hook is triggered when Nuxt is about to serialise the server side data to send it to the client side `window.__NUXT__` variable. So we give it some help here by telling him how to deal with the variables that are class instance.

The point we're still missing here is **how to identify the objects that actually needs that parsing**. We'll get back to this part later.

### 2. [Client side] Deserialising back to instances

The server side data is now properly serialised. But it's still only POJO, not class instances.

Now, from the client, we have to **deserialise it to create new class instances!**

On client side, Nuxt doesn't provide - yet? - any custom hooks for SSR data handling, like the `vue-renderer` hook for custom SSR code.

So the easiest solution I've came up with is to use the `beforeCreate` hook in the page we are using this data.

In order to be DRY, I created a custom decorator to handle that. It's used like this:

```typescript
export default TodoPage extends Vue {
  @SerializeData(Todo)
  todo!: Todo

  asyncData ({ $route }) {
     const todoObj = axios.get(`/api/todos/${$route.params.id}`).then(res => res.data)
     return {
        todo: plainToClass(Todo, todoObj),
     }
  }
}
```

The decorator serves two objectives:
 1. Identify which data property has to be (de)serialised.
 2. Provide which constructor to use for this specific property.

Internally, it enriches the `beforeCreate` hook on client side to deserialise the data from the SSR POJO received.

Here is what it looks like:

```typescript
import Vue, { ComponentOptions } from 'vue'
import { ClassConstructor, plainToClass } from 'class-transformer'
import { createDecorator } from 'vue-class-component'

/** Decorator to deserialise SSR data on client side with the given constructor
 * @param classType The class constructor to use for this property
 */
export const SerializeData = <T> (classType: ClassConstructor<T>) => createDecorator((options, key) => {
  // On client side only
  if (process.client) {
    wrapBeforeCreate(options, key, classType)
  }
})

/** Enrich the beforeCreate hook with a deserialiser function. Ensure we still call the original hook if it exists. */
function wrapBeforeCreate <T> (options: ComponentOptions<Vue>, key: string, classType: ClassConstructor<T>) {
  const originalBeforeCreateHook = options.beforeCreate
  options.beforeCreate = function deserializerWrapper (...args) {
    deserializer.call(this, key, classType)
    originalBeforeCreateHook?.apply(this, args)
  }
}

/** Deserialise a POJO data to a class instance 
 * @param key the property name
 * @param classType The class constructor used to create the instance
 */
function deserialiser <T> (this: Vue, key: string, classType: ClassConstructor<T>) {
  const { data } = this.$nuxt.context.nuxtState || {}

  const [asyncData] = data // not really sure why it's an array here tbh.
  if (asyncData && asyncData[key]) {
    // Convert back the data to a class instance
    asyncData[key] = plainToClass(classType, asyncData[key])
  }
}
```

When the component is compiled down to javascript, it should be looking like this:

```javascript
export default {
  asyncData() {
     const todoObj = axios.get(`/api/todos/${$route.params.id}`).then(res => res.data)
     return {
        todo: plainToClass(Todo, todoObj),
     }
  }
  
  beforeCreate() {
     deserialiser('todo', Todo)
  }
}
```

Now, when using the decorator, the POJO data will be transformed to a class instance when the page is rendering! 🎉

### 3. Polishing the server side

With this decorator, we can improve the server side deserialiser to **identify the properties** instead of trying to convert them all to POJOs.

The idea is simple: **we can register a temporary data to be used by our custom renderer hook.**

Here is the final code of the decorator:

```typescript
import Vue, { ComponentOptions } from 'vue'
import { ClassConstructor, plainToClass } from 'class-transformer'
import { createDecorator } from 'vue-class-component'

/** Decorator to handle SSR data as class instances
 * @param classType The class constructor to use for this property
 */
export const SerializeData = <T> (classType: ClassConstructor<T>) => createDecorator((options, key) => {
  if (process.server) {
    wrapAsyncData(options, key)
  } else {
    wrapBeforeCreate(options, key, classType)
  }
})

/** Enrich the asyncData hook with a registering function.
 * Ensure we still call the original hook if it exists.
 */
function wrapAsyncData (options: ComponentOptions<Vue>, key: string) {
  const originalAsyncDataHook = options.asyncData
  options.asyncData = async function wrapperAsyncData (...args) {
    const originalAsyncData: Record<string, any> = (await originalAsyncDataHook?.apply(this, args)) || {}
    
    registerSerializableProp(originalAsyncData, key)
    
    return originalAsyncData
  }
} 

/** Add a config property to store the data that must be serialised */
function registerSerializableProp (asyncData: any, key: string) {
  asyncData.serializerConfig = asyncData.serializerConfig || []
  asyncData.serializerConfig.push(key)
}

/** Enrich the beforeCreate hook with a deserialiser function.
 * Ensure we still call the original hook if it exists.
 */
function wrapBeforeCreate <T> (options: ComponentOptions<Vue>, key: string, classType: ClassConstructor<T>) {
  const originalBeforeCreateHook = options.beforeCreate
  options.beforeCreate = function deserializerWrapper (...args) {
    deserializer.call(this, key, classType)
    originalBeforeCreateHook?.apply(this, args)
  }
}

/** Deserialise a POJO data to a class instance 
 * @param key the property name
 * @param classType The class constructor used to create the instance
 */
function deserialiser <T> (this: Vue, key: string, classType: ClassConstructor<T>) {
  const {data} = this.$nuxt.context.nuxtState

  const [asyncData] =data
  if (asyncData && asyncData[key]) {
    asyncData[key] = plainToClass(classType, asyncData[key])
  }
}
```

The new part is ran only for server side (notice the `process.server` at the beginning of the decorator function).

We create a `serializerConfig` property that stores all the keys that we have to serialise.

Going back to our custom hook:

```javascript
context (context) {
  if (Array.isArray(context.nuxt.data)) {
    const data = context.nuxt.data[0] || {}
    // If we have a `serializerConfig` property
    if (Array.isArray(data.serializerConfig)) {
      // Loop on all its values
      data.serializerConfig.forEach((dataKeyToSerialise) => {
        data[dataKeyToSerialise] = classToPlain(data[dataKeyToSerialise])
      })
      // Remove the temporary object, now obsolete.
      delete data.serializerConfig
    }
  }
},
```

And this is it! We have a fully functional class instance transfer in Nuxt SSR!

# Conclusion

By reading this article, we learnt that:
* SSR can't deal with class instances out of the box
* We can develop a workaround for this
* Nuxt SSR engine provides helpful hooks

Summary of the solution provided:
* Create a custom `SerialiseClass` decorator to identify the component properties to be serialised manually
* Listen to the Nuxt `vue-renderer:ssr:context` hook to convert the identified class instances to POJO
* Use the decorator to deserialise the data back to class instances on client side with the `beforeCreate` lifecycle hook.

It sure is subject to further improvements, as I may not know some magic trick that could handle that more easily.

Thank you very much for reading my first article! I'm opened to any feedback (about the article content, typos, ideas etc.) and questions.

Have a great day! 🙌