January 1st, 2022

Type-Safe Server-Side Templates in Express using JSX and React

I started hacking on a side project during the holiday break and ended up giving TypeScript, TypeOrm, and Express a try. I’m very impressed with the level of type safety the combination provides but was disappointed that the view layer was type-unsafe. I started with handlebars but after a few bugs, I decided to spike out using JSX/React as a server-side templating language and was surprised at how easy it was to set up.

Why server-side JSX has potential

The benefits of components have already proven themselves to be valuable on the front-end, but here’s a few reasons why I think they’re valuable as a server-side templating engine too.

  • Type safety – when code changes or if I forget to pass a parameter to the template you get immediate feedback.
  • Re-usability – Existing templating engines have spotty support for partials but components offer re-usability as a core feature.
  • Re-usability, again - I haven’t needed it yet, but components can likely be shared between the front-end and back-end easily.
  • Familiarity - Components are now mainstream so a large number of developers will be immediately familiar with how to write and structure component based applications, server-side or client-side.

Dependencies

The set-up was surprisingly simple thanks to the React team. All you need is React, ReactDOM (for ReactDOMServer), the relevant TypeScript types, and the correct TypeScript configuration.

Here’s what I did to get my app ready:

  1. Add jsx: "react-jsx", to the compilerOptions so we can use JSX in .tsx files. Here’s what that may look like:

    {
      "compilerOptions": {
        "module": "commonjs",
        "removeComments": true,
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "target": "es2017",
        "sourceMap": true,
        "incremental": true,
        "strictBindCallApply": true,
        "noImplicitAny": true,
        "strictNullChecks": true,
        "forceConsistentCasingInFileNames": true,
        "noFallthroughCasesInSwitch": true,
        "jsx": "react-jsx"
      }
    }
    
  2. Install React and ReactDOM npm i react react-dom so we can write and render components.
  3. Install the types for TypeScript npm i --save-dev @types/react @types/react-dom.

With this complete, we should now have everything we need to start writing JSX in our handlers.

Rendering Components Server-Side

Now that our dependencies are installed, we can write our function for rendering JSX. Thanks to the React provided API’s we’re able to get away with a relatively simple function:

render(response: express.Response, el: ReactElement) {
  response.write('<!DOCTYPE html>\n');
  const markup = ReactDOMServer.renderToStaticMarkup(el);
  response.write(markup);
  response.end();
}

The first thing we do is write the doctype, unfortunately JSX doesn’t support a top-level doctype comment so we have to write it ourselves. Next, we use renderToStaticMarkup to take our component and turn it into HTML. Finally, we write the markup to the response and close it to complete our request/response.

N.B. renderToStaticMarkup is used instead of renderToString since it strips out React attributes that are used when re-hydrating a React application on the front-end.

Now that we have the helper available, we’re able to use it in a handler. My application has an abstraction around express handlers, but I’ve converted it to be closer to express for this example.

// app.tsx
render(response: express.Response, el: ReactElement) {
  response.write('<!DOCTYPE html>\n');
  const markup = ReactDOMServer.renderToStaticMarkup(el);
  response.write(markup);
  response.end();
}

app.get("/teams/:id", async (req, res) => {
  if (!response.locals.currentUser) {
    render404(res)
    return;
  }

  const slug = req.params.id;
  const team = await entityManager.findOne(Team, { slug: slug });

  if (!team) {
    render404(res)
    return;
  }

  render(
    <Layout currentUser={this.currentUser}>
      <h1 class="text-lg mb-3">{team.name}</h1>
    </Layout>,
  );
})

With this render helper we’ve taken a traditionally type unsafe layer of our application and made it type-safe. This eliminates an entire class of bugs that would usually only be caught through unit testing or in production (e.g. 500 errors).

Conclusion

I didn’t expect the difference in productivity between a type-safe and a type-unsafe view layer to be so different. I love that the compiler can catch small mistakes in my editor, compared to the traditional approach of making a change and reloading the page.

This isn’t a new idea1, but I’m happy with this approach so far and I’m excited to see how it can be improved over time. If your application has a type-unsafe view layer I recommend giving this approach a try, I think you’ll be happily surprised with the benefits.

  1. Next.js has been using React for SSR for quite a while now and is worth checking out if you’re looking for a more front-end oriented solution.