rigotti.nl

TSX as template engine for Nest

Published on Jul 14, 2025

Lately, I have been entertaining the idea of making a full-stack web application using Nest. The concept is not novel, as their own documentation defines how to achieve an MVC model by adding an Express-supported template engine before the rendering step. Since I wanted to have a fully type-safe application, I wanted to use TSX as the render engine, but it turned out not to be as straightforward as I thought.

Why TSX?

To use a template engine like Handlebars in Nest is easy. Following the documentation, you can get the following working.

// index.hbs
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>App</title>
  </head>
  <body>
    <div id="surfers-list">
      {{ surfers }}
    </div>
  </body>
</html>
// src/app.controller.ts

import { Get, Controller, Render } from '@nestjs/common';

@Controller()
export class AppController {
  @Get()
  @Render('index') // points to index.hbs
  root() {
    return { surfers: 'Yago, Italo, Gabriel' };
  }
}

My issue with that approach is that it is not type-safe. If I change the data passed from the controller to the view, or vice-versa, there's no guardrail to let me know about a possible mistake. If I apply the following change to my view, there's nothing telling me that the property surfers now expects a list.

<ul id="surfers-list">
  {{#each surfers}}
    <li>{{this}}</li>
  {{/each}}
</ul> 

The following change would be necessary, but again, there's nothing preventing me from sending the wrong data.

@Get()
@Render('index')
root() {
  return { surfers: ['Yago', 'Italo', 'Gabriel'] };
}

What if I could write my view in TSX instead?

// views/index.tsx

export interface HomepageProps {
  surfers: string[];
}

export default function Homepage({ surfers }: HomepageProps) {
  return (
    <div id="surfers-list">
      <ul>
        {surfers.map((s) => (
          <li key={s}>{s}</li>
        ))}
      </ul>
    </div>
  );
}

And use it like this inside my controller?

import Homepage, { HomepageProps } from '../views/index';

@Get()
@Render(Homepage)
root(): HomepageProps {
  return { surfers: ['Yago', 'Italo', 'Gabriel'] };
}

This would bind the returned value from the controller to the view properties. Nice, right?

Setting it up

As I expected, I was not the first one who had tried integrating TSX with Nest. The most promising package I found was nestjs-tsx-views, but, even though I followed the installation process, it didn't work for me. The project is also five years old and seems unmaintained, which sent me searching for another alternative.

The best resource I found was the Medium post JSX and Nest.JS together?, where the author explains the process of supporting TSX by creating a new decorator called @JsxRender and patching the underlying Express render method to call React.createElement to render the view. After following the steps, I got it working!

import Homepage, { HomepageProps } from '../views/index';
import { JsxRender } from './jsxrender.decorator';
...

@Get()
@JsxRender(Homepage) // instead of `@Render`
root(): HomepageProps {
  return { surfers: ['Yago', 'Italo', 'Gabriel'] };
}

Not there yet

Even though I've achieved what I was aiming for, I was not completely happy with this solution. My biggest gripe was the requirement to attach the HomepageProps as a return value if I want to be type compliant. For instance, the following code would still be valid but not type-safe.

@Get()
@JsxRender(Homepage)
root() {
  return { surfers: 'no complaints...' }; // should be a string[]
}

What if I could make the whole controller a tsx file? It turns out, that after making some changes to the @JsxRender decorator, I could achieve this.

// src/app.controller.tsx <-- renamed!

import React from 'react';
import { Get, Controller } from '@nestjs/common';
import Homepage from '../views/index';
import { JsxRender } from './jsxrender.decorator';

@Controller()
export class AppController {
  @Get()
  @JsxRender()
  root() {
    return <Homepage surfers={['Yago', 'Italo', 'Gabriel']} />;
  }
}

Yes, I can also just return the JSX element directly and avoid exporting/importing props at all.

Limitations

It's worth mentioning that even though TSX (or JSX) is being used as a template engine, we are not hydrating these components. This, in practice, means that APIs such as hooks that add interactivity to components won't work here. I am yet to build something more complex on top of it that should expose some hard edges, but this situation is personally better for me than using Handlebars.

This setup also doesn't support hot-reloading, which I will look into soon for an option. If you have any idea, please reach out to me, or submit a PR in the repository.

You can find the source code here.