After completing this tutorial, you'll be able to use React to render templates server side as part of a Node.js application. This will be set up in a way for the TypeScript compiler to catch any issue with rendering the view, including incorrect view model data and dealing with renamed files.

I used Visual Studio Code for the tutorial, since it's a great cross platform editor/IDE, but you can use any tools you like. Other popular options include WebStorm.

The Problem

Before showing how to do this, I should show why one would want to do it in the first place. Most people only consider React in the realm of client side SPA frameworks. We've been using view engines like Handlebars and Pug for years. What's changed? TypeScript is here now. Now that the Node.js world has a compiler, there's value in integrating our entire work flow into it.

Those new to TypeScript should check out its main site. It's a compile time tool that "transpiles" down to whichever level of JavaScript you need, be it Node.js or code capable of running in older web browsers. Their quickstart shows how to use it in many popular frameworks.

TypeScript catches lots of errors at compile time, but here's where it fails us when it comes to traditional templating engines. To be framework agnostic, the following is pseudocode. It demonstrates the usual way to render a view. A function representing the controller action passes a string with a template's name and some data to a function tasked with rendering the view:

function myControllerAction(response: FrameworkHttpResponse) {
  response.render('home_page', { msg: 'Hello, World!' });
}

The compiler's errors and the IDE's code completion can warn us if we misspell the response parameter or the render method, or if we don't use the proper syntax when creating the object literal with the "Hello, World!" message.

However, the first argument to the render function is "stringly-typed". The compiler knows nothing about the string. Without custom extensions for the IDE meant to understand the templating engine, there's no way for the IDE to warn us if renaming the view file breaks this function call.

There's also no way for the IDE to understand whether or not that data makes sense for that view. You've got strong typing up until the data gets sent to the view to be rendered.

This made sense in the pre-TypeScript world. JavaScript has no compiler. There was no urgency to deal with this string typing issue because the entire code base would be weakly typed anyways. In the post-TypeScript world, we need a better way to render views.

Enter JSX

This is the secret sauce here. We'll be using JSX to create our views, and we'll set up the TypeScript compiler to check it for us. JSX is a programming language that looks like XML and transpiles to JavaScript expressions. In English? This:

<div>
  <p>Hello, World!</p>
</div>

transpiles to:

React.createElement("div", null,
  React.createElement("p", null, "Hello, World!"))

React users, most of whom use it to create client side SPAs, can use this syntax to use a code-driven approach as they create templates for their views. They can even use JavaScript functions like filter and map to enumerate over arrays of data, converting them to child HTML elements inline in the JSX:

<div>
  <ul>
    {[1, 2, 3].map(n => (<li>{n}</li>))}
  </ul>            
</div>

transpiles to JavaScript capable of rendering:

<div>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
</div>

If you're already familiar with React and JSX, then I'm preaching to the choir and need say no more. If you're new to this, I recommend checking out Facebook's official React documentation which explains JSX in more detail.

Integrating React with Node.js and Adding TypeScript

Now that we know what we'll be working with, it's time to put this together. We'll start with a new app. Create a new app with the yarn init (or npm init) command, and create a src directory for our source code. After that, your directory structure will look like this:

src/
package.json

To upgrade this JavaScript app to a TypeScript app, we must add TypeScript as a dev dependency, with the yarn add -D typescript or npm install --save-dev typescript command. Because TypeScript can be thought of as a tool specific to building this project, whose version changes often and should be checked in, it is best to use it as a dev dependency, not globally installed on the system.

A tsconfig.json file describes how the TypeScript compiler will interpret your code as it builds it. Rather than create this from scratch, an effective way to bootstrap your new TypeScript Node app is to use the tsconfig.json file from Microsoft's TypeScript-Node-Starter repo.

After adding this to your app, your directory structure will look like this:

node_modules/
src/
package.json
tsconfig.json

Because the point of this tutorial isn't to focus on controller or model design, we won't worry about databases or separating out our source code into many directories. All we need to render HTML to the browser is a simple HTTP server. Using Express will be less verbose than using raw Node.js, so we'll add it with yarn add express or npm install --save express.

To be able to use the Express TypeScript type definitions in our IDE, we can add them with yarn add -D @types/express. Again, since they're only useful in the context of building our app (not running it), we should install them as dev dependencies.

Create a file in the src directory called index.ts for the entry point of our Node.js app. We can use the following code for the file, which will render some HTML to the browser when visitors send a request to http://localhost:3000:

// src/index.ts

import * as express from 'express';

const app = express();

app.get('/', function (req, res) {
  const resData = `<!doctype html>
    <html>
      <head>
        <title>App</title>
      </head>
      <body>
        <p>Hello, World!</p>
      </body>
    </html>
  `;
    
  res.send(resData);
});

const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);

At this point, your directory structure will look like this:

node_modules/...
src/
  index.ts
package.json
tsconfig.json

To test building this app, run the TypeScript compiler with the command ./node_modules/typescript/bin/tsc from the project directory, or use the tool in your IDE appropriate to compile the TypeScript.

If you're using Visual Studio Code, you can do this with the "build" task which will be automatically available under Tasks > Run Build Task....

If you're using WebStorm, a good option is to use the "Compile TypeScript" pre-launch task. However, WebStorm users must remove the "esModuleInterop": true property from the tsconfig.json file.

After the compile step completes, you can run the app with the command node dist/index.js (or use an appropriate tool in your IDE to launch the app).

Creating a React Component for the Home Page View

Now let's create a React component to be a view for our page. We'll need some more NPM modules, and their type definition files. Use the commands yarn add react react-dom and yarn add -D @types/react @types/react-dom or npm install --save react react-dom and npm install --save-dev @types/react @types/react-dom.

The react package contains code for the main React constructs like components (which we'll use to build our views). The react-dom package contains code to assist with either linking to the DOM (which client side apps do, and we won't) or rendering the components into static HTML that the server can send in the response (which we will do).

Create a directory called views for our views, and a file called Home.tsx in the directory for our first component, which will be for our Home page view:

// src/views/Home.tsx

import * as React from 'react';

export class Home extends React.Component {
  render() {
    return <div>
      <p>Hello, World!</p>           
    </div>;
  }
}

At this point, your directory structure will look like this:

node_modules/...
src/
  views/
    Home.tsx
  index.ts
package.json
tsconfig.json

Adding JSX Support to TypeScript Compiler

At this point, you will notice an issue when trying to compile the app (with the tsc command) or from IDE warnings:

[ts] Cannot use JSX unless the '--jsx' flag is provided

The TypeScript compiler is unable to handle the JSX syntax without the --jsx flag added. We can fix this by adding it to the tsconfig.json file. After adding the flag, it will look like this:

{
  "compilerOptions": {
    "module": "commonjs",
    "esModuleInterop": true,
    "target": "es6",
    "noImplicitAny": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "paths": {
      "*": [
        "node_modules/*",
        "src/types/*"
      ]
    },
    "jsx": "react"
  },
  "include": [
    "src/**/*"
  ]
}

Rendering the View

To render the view, we can rename index.ts to index.tsx and import the component from Home.tsx into it. Why the rename? We need this source code file to understand JSX syntax now, so it must have the .tsx file extension. We can then use the renderToStaticMarkup function from the react-dom package to render the component as a string:

// src/index.tsx

import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';

import { Home } from './views/Home';

const app = express();

app.get('/', function (req, res) {
  const resData = `<!doctype html>
    <html>
      <head>
        <title>App</title>
      </head>
      <body>
        ${ReactDOMServer.renderToStaticMarkup(<Home />)}
      </body>
    </html>
  `;
    
  res.send(resData);
});

const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);

In this example, our Home component represents the markup inside the body of our page, so we insert the rendered string into the body element in the string template.

If we compile and run this (with the command node dist/index.js), we will see the "Hello, World!" message rendered.

DTOs

Now we can begin to explore the power this work flow gives us as we build our app or when we're maintaining it and our domain model changes. We'll use a concept called DTOs ("data transfer objects"), also known as "view models" or "resource models". It's a useful design pattern. Rather than work directly with database access logic to build a view, you delegate to a middle layer, often called a "service", tasked with getting the data you need for that specific operation. In a web app, these operations are HTTP request & response cycles.

For example, let's consider GitHub. The operation might be loading the data needed to display the nav bar at the top of every page. This is isn't going to include everything about your profile (no description, no links to your social media accounts) but it will probably include some data about the repositories you interact with. It will be able to display a different looking "notification" icon if you've got notifications to look at, perhaps a blue dot:

github_nav_notification

In this example, the DTO doesn't line up with just one table of data in an SQL database. This is normal. In the real world, you may have to get data from more than one table in an SQL database, or even fetch data from multiple databases, caches, remote web services, or a combination of sources like these. That's why DTOs are so important. You can maintain a good understanding of the parts of your app by describing the data each operation needs.

With a strongly typed programming language, like TypeScript, this data can be checked by the compiler as it flows through your app into your views. And that's exactly what we're setting up here. The TypeScript compiler will refuse to compile the app if a front end developer adds a member to the DTO class - it would be a missing member in the controller or service layer. In the opposite situation, with a back end developer adding a member to the DTO class, the compiler won't complain about the member being unused in the view, but at least now the front end developers will have code completion to help them use it.

In TypeScript, to create DTOs, we can use a class with public members instead of an interface for them since this allows them to have methods used for validation, and validation is important in the context of an HTTP request/response cycle.

Create a directory called dtos in the src directory of your app. Inside this directory, create the file HomeDto.tsx, and inside, we'll create a class called HomeDto for the DTO. After creating this file, your directory structure will look like this:

node_modules/...
src/
  dtos/
    HomeDto.tsx
  views/
    Home.tsx
  index.ts
package.json
tsconfig.json

We can use the following code for our HomeDto class:

// src/dtos/HomeDto.tsx

import * as React from 'react';

export class HomeDto {
  backgroundColor: string;
  currentDate: Date;
}

Readers with previous ES6/TypeScript knowledge will note that we're not using default exports, which is a bit less verbose on the import side and pretty common. We do this to allow our import statements to be checked by the TypeScript compiler. Later in the tutorial, we'll see examples of where changes in the code base (like renaming a class but not renaming the import in this case) would be caught by the compiler as an error.

Since the goal of this tutorial is only to demonstrate using JSX to render views, we'll skip databases, controllers, etc, and later in the tutorial we'll simply pass example instances of this class to the views to render them.

Using DTOs as React Props

React uses the "props" (meaning "properties") concept to pass data into components. This is similar to passing an argument into a function call, so that the data is available in the body of the function as a parameter. These properties can be hard-coded values or dynamic, based on runtime data. The values of the props can be any valid JavaScript value. You can read more about this in React's official documentation. This ends up looking like this in JSX:

<MyComponent myFirstProp={'myFirstPropValue'} mySecondProp={2} />

With TypeScript available to us, we can make these props stongly typed, so that not making our component declaration match the props required would trigger a compiler error. We do this by making an interface to represent the data passed in as props and including the interface in the type that the component extends.

Due to TypeScript's type system, we can also use a class for the props generic type. This enables us to use our DTO classes for the component's props. After adding an import for HomeDto and adding it as a type parameter, our Home class (in src/views/Home.tsx) will look like this:

// src/views/Home.tsx

import * as React from 'react';

import { HomeDto } from '../dtos/IndexDto';

export class Home extends React.Component<HomeDto> {
  render() {
    return <div>
      <p>Hello, World!</p>           
    </div>;
  }
}

Once we do this, we will have access to a member called props inside the render function, which will contain the members of the HomeDto class. This is where we can get code completion help as we add the background color and current date data from our example:

props_intellisense

When we're finished adding to our render method, it will look like this:

render() {
  return <div style={{ backgroundColor: this.props.backgroundColor }}>
    <p>Hello, World! The date is {this.props.currentDate.toLocaleDateString()}</p>
  </div>;
}

Note that there are a few quirks with JSX compared to normal HTML. The styles can be inlined, but the style attribute's value must be an object. The names of the CSS properties are camelcased and used as properties of this object. Since we use braces to describe JavaScript expressions within JSX, we see this nested {{}} brace syntax when using an object as a JSX attribute value.

To provide values for the props, our app's index.tsx file will be modified to the following:

// src/index.tsx

import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';

import { Home } from './views/Home';

const app = express();

app.get('/', function (req, res) {
  const resData = `<!doctype html>
    <html>
      <head>
        <title>App</title>
      </head>
      <body>
        ${ReactDOMServer.renderToStaticMarkup(<Home backgroundColor={'red'} currentDate={new Date()} />)}
      </body>
    </html>
  `;
    
  res.send(resData);
});

const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);

Shared Layouts via Nested React Components

Traditional view templating engines use the concept of "partial" or "shared" layouts to allow you to have a common shell for your views, with the contents of that shell unique for each particular view. In React, we accomplish this by creating a component for our shell and rendering the inner component relevant to the view inside it. We can also create components to use on many views (such as nav elements) and render them as siblings to the view specific components where appropriate. The options for composability are very flexible.

Here's an example that might work well for most apps. Create two additional views, Shell.tsx and Nav.tsx. Your directory structure will then look like this:

node_modules/...
src/
  dtos/
    HomeDto.tsx
  views/
    Home.tsx
    Nav.tsx
    Shell.tsx
  index.ts
package.json
tsconfig.json

Use the following code for the Shell component:

// src/views/Shell.tsx

import * as React from 'react';

// A component used to wrap our view components so that they form
// complete HTML pages.
export class Shell extends React.Component {
  render() {
    return <html>
      <head>
        <title>App</title>
      </head>
      <body>
        {this.props.children}
      </body>
    </html>;
  }
}

Note that there is no need to create a DTO class for the Shell component, and therefore no need to import it into Shell.tsx or use it as a type parameter as we extend React.Component.

Also note how we use this.props.children in the body element. Every React component can have no children, one child, or many children. This optional property of the React.Component class is available to the render method. It will evaluate to any components that are nested as its children, which we'll see in action shortly when we modify our Index view (using the Shell component as its outer component).

Use the following code for the Nav component:

// src/views/Nav.tsx

import * as React from 'react';

interface Props {
  activeRoute: string;
}

export class Nav extends React.Component<Props> {
  render() {
    const msg = ' <- You are here! :)';

    return <ul>
      <li><a href="/">Home</a>{this.props.activeRoute === '/' ? msg : ''}</li>
      <li><a href="/about">About</a>{this.props.activeRoute === '/about' ? msg : ''}</li>
    </ul>;
  }
}

Note that while this component doesn't use a DTO (it has nothing to do with the domain model), it uses an interface for its props. This is because the component is meant to be used by more than one view for the app, and its props interface lets us strongly type its parameters (in this case, the name of the active route). We don't expect this component to have children, so we omit this.props.children from its render method, just like we did for our Home view component.

Now we can revisit our index.tsx file and add a second route (/about), using our Shell component as a base for the views of each route. The Nav component is a sibling of the view-specific components, and those two components become the two child components of the Shell component:

// src/index.tsx

import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';

import { Home } from './views/Home';
import { Nav } from './views/Nav';
import { Shell } from './views/Shell';

const app = express();

app.get('/', function (req, res) {
  const resData = `<!doctype html>
    ${ReactDOMServer.renderToStaticMarkup(<Shell>
      <Nav activeRoute={'/'} />
      <Home backgroundColor={'red'} currentDate={new Date()} />
    </Shell>)}
  `;
    
  res.send(resData);
});

app.get('/about', function (req, res) {
  const resData = `<!doctype html>
    ${ReactDOMServer.renderToStaticMarkup(<Shell>
      <Nav activeRoute={'/about'} />
      <Home backgroundColor={'blue'} currentDate={new Date()} />
    </Shell>)}
  `;
    
  res.send(resData);
});

const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);

For simplicity, we won't create a new view for the About page. We'll just reuse the Home page's view. Therefore, the Home component is present in both Shell components.

Notice as you build the new complete views for each route in index.tsx that linters, the compiler, Intellisense etc will kick in to warn you if you omit a JSX attribute, like omitting the activeRoute attribute from the Nav component:

props_intellisense_on_nav

You'll get a helpful error message like:

Property 'activeRoute' is missing in type '{}'.

Reduce Boilerplate with a Render Function

The only thing left to do to help us with our work flow adding new routes is to remove some of the boilerplate that we have. We can't include the <!doctype html> directive in our Shell component. We can move this into a function with a shorter name. Create a file called utils.ts in the project directory. The .tsx extension isn't mandatory because we won't be using JSX syntax inside our helper function. Use the following code for utils.ts:

// src/utils.ts

import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';

export function renderReactView(el: React.ReactElement<any>): string {
  return `<!doctype html>${ReactDOMServer.renderToStaticMarkup(el)}`;
}

The renderReactView function's parameter el will allow it to accept any React component. The function is exported so we can use it elsewhere in our app.

At this point, our directory structure will look like this:

node_modules/...
src/
  dtos/
    HomeDto.tsx
  views/
    Home.tsx
    Nav.tsx
    Shell.tsx
  index.ts
  utils.ts
package.json
tsconfig.json

After importing the helper function into index.tsx and using it where appropriate, its code will look like this:

// src/index.tsx

import * as express from 'express';
import * as React from 'react';
import * as ReactDOMServer from 'react-dom/server';

import { Home } from './views/Home';
import { Nav } from './views/Nav';
import { renderReactView } from './utils';
import { Shell } from './views/Shell';

const app = express();

app.get('/', function (req, res) {    
  res.send(renderReactView(<Shell>
    <Nav activeRoute={'/'} />
    <Home backgroundColor={'red'} currentDate={new Date()} />
  </Shell>));
});

app.get('/about', function (req, res) {
  res.send(renderReactView(<Shell>
    <Nav activeRoute={'/about'} />
    <Home backgroundColor={'blue'} currentDate={new Date()} />
  </Shell>));
});

const port = 3000;
app.listen(port);
console.log(`Listening on port ${port}`);

That's it! We now have the ability to use JSX to build our views on a server side app. And the TypeScript compiler and IDEs it is linked to will warn us about type issues with the data for the views. This means you don't need traditional IDEs like Visual Studio or IntelliJ IDEA-based IDEs to benefit from code completion for your views.

Long Term View Code Completion

Here's where this will help us as we continue to build and maintain our app.

New Data for DTO

Suddenly, we need to include more data in our DTO. Users now have profile photos, so we now have a profilePhotoUrl property added to the HomeDto class. It isn't an optional property, meaning we expect our hypothetical service classes to fetch this for us and we're expected to always display it in the view:

// src/dtos/HomeDto.tsx

export class HomeDto {
  backgroundColor: string;
  currentDate: Date;
  profilePhotoUrl: string; // uh oh. Time to fix the views
}

Well, that was easier than expected. We're warned about the missing data:

props_intellisense-1

Renaming Source Code Files and Classes

Let's examine another scenario that can cause issues - renaming things. All of the following situations will result in us being warned about the breakage at the import statement:

  • Renaming the source code file. For example, Home.tsx to HomePage.tsx.
  • Renaming a class. For example, renaming the HomeDto class to HomePageDto.
  • Renaming a file, but with a typo or a simelar error.

blog_type_safety_renamed_file

Fun and Profit

By linking to the compiler, we get a lot of functionality for free. There's a big push for cross platform development tools, and I've discovered that this is a great way to succeed with that goal. A big thanks goes out to all the work the teams behind TypeScript, Visual Studio Code, and React have done to give us these great tools!


The code for this tutorial is available on GitHub.