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:
-
Add
jsx: "react-jsx",
to thecompilerOptions
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" } }
- Install React and ReactDOM
npm i react react-dom
so we can write and render components. - 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.