How to setup Nodejs, EJS with Typescript using Gulp and Webpack

This post assumes that you want to use typescript for the backend as well as frontend (using ejs template) in node, but struggling to set up a system to transpile the typescript code to javascript for the backend as well as frontend which a browser can understand.

The source code is available on GitHub here.

Basic setup

To continue with the post you need to have node and npm installed in your system

Let's create a node project

npm init -y 

Setting up Node with typescript

In order to work with typescript in node, we need to install some dev dependency that would help us to run typescript with nodemon.

npm i -D typescript ts-node @types/node

the @types install the type alias for the package.

Now let's install nodemon

npm i -g nodemon

Let's install express

npm i express

We would also need the types alias for express. So let's install that too

npm i -D @types/express

Now let's initiate a tsconfig.json

tsc -init

Now, installing the basic dependencies is done, let's define the directory structure.

├── node_modules/
├── dist/
├── src/
│   ├── config/
│       └── debug.ts
│   ├── public/
│   │   ├── assets
│   │   └── ts/
│   └── views/
├── app.ts
├── package-lock.json
├── package.json
└── tsconfig.json

Whatever code that we would write would be in the src directory and the build would be in the dist directory.

The typescript for the frontend would be in the ts folder which is inside the public folder.

All the ejs template file would go into the views directory.



Configure your tsconfig.json

Since we would be using typescript for node, we need to make some changes in the tsconfig.json file.

  1. "moduleResolution": "node"
  2. "rootDir": "./src"
  3. "outDir": "./dist"
  4. "noImplicitAny": true

This would be enough to get us working. If you would like to know more options in tsconfig, check out tsconfig documentation

So the final tsconfig.json would look like this

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "noImplicitAny": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Now let's write the code for getting the server up and running.

import express, { Application } from "express";
import http from "http";
import debug from "./config/debug";

const app: Application = express();
const server: http.Server = http.createServer(app);

// Setting the port
const port = debug.PORT;

// Starting the server
server.listen(port, () => {
    console.log(`SERVER RUNNING ON ${port}`);
});

Now run the server

nodemon src/app.ts

You should see

SERVER RUNNING ON 3000

This is all good, nodemon is handling everything, but now if you notice there is no dist directory being generated. Let's use gulp to generate the dist directory and nodemon to watch for changes. Both would help us automate the entire process

Let's install gulp, gulp-typescript, and del

npm i -D gulp gulp-typescript del

Now on the root directory of the project create a gulpfile.js file

gulpfile.js

var gulp = require("gulp");
var ts = require("gulp-typescript");
var tsProject = ts.createProject("tsconfig.json");
var del = require("del");


// Task which would transpile typescript to javascript
gulp.task("typescript", function () {
    return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist"));
});

// Task which would delete the old dist directory if present
gulp.task("build-clean", function () {
    return del(["./dist"]);
});

// Task which would just create a copy of the current views directory in dist directory
gulp.task("views", function () {
    return gulp.src("./src/views/**/*.ejs").pipe(gulp.dest("./dist/views"));
});

// Task which would just create a copy of the current static assets directory in dist directory
gulp.task("assets", function () {
    return gulp.src("./src/public/assets/**/*").pipe(gulp.dest("./dist/public/assets"));
});

// The default task which runs at start of the gulpfile.js
gulp.task("default", gulp.series("build-clean","typescript", "views", "assets"), () => {
    console.log("Done");
});

Now run the file by

gulp

After the default task finishes check your directory, you would see a dist directory generated. It should have the config, public/ts, public/asset, views directory with app.js

Now run the server using

node dist/app.js

You should see

SERVER RUNNING ON 3000

To automate the server which listens to file changes and builds and re-runs the server again, we need to install another package.

npm i -D npm-run-all

Now we need to create some scripts in order for the server to watch for changes and re-run the server again with the new changes.

In your package.json add this code inside your scripts object

{
    "start:gulp": "npm-run-all gulp start",
    "gulp": "gulp",
    "start": "node ./dist/app.js",
    "dev": "nodemon --watch src -e ts,ejs --exec npm run start:gulp"
}

Basically what this does is check for changes and if found then re-runs the gulp command which transpile the typescript code and then runs the server using start.

You should again see

SERVER RUNNING ON 3000

So now we have typescript running with nodejs using gulp.

Setting up EJS templates

To work with ejs templates in node we need to install some npm packages. Let's install them

npm i express-ejs-layouts ejs

Now after installing we need express to know that we want to use ejs as the view engine and add the root path for the views so express knows where they are.

Add these in app.ts

import expressLayouts from 'express-ejs-layouts';

// EJS setup
app.use(expressLayouts);

// Setting the root path for views directory
app.set('views', path.join(__dirname, 'views'));

// Setting the view engine
app.set('view engine', 'ejs');

Now let's add some ejs templates into the mix

Create a new file under views and name it layout.ejs

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Node Typescript</title>
  </head>
  <body>
    <div><%- body %></div>
  </body>
</html>

Now save it and add another file and name it index.ejs in the same directory as layout.ejs

<h1>Hello world</h1>

<!-- This is the script that we would be creating later -->
<script src="./js/scripts.js"></script>

And save the index.ejs file. Now, since the templates are ready we would need a route where we can render this page. So let's create that

In your app.ts file add

import { Request, Response } from "express";

/* Home route */
app.get("/", (req: Request, res: Response) => {
	res.render("index")
});

And now run the server and go to http//:localhost:3000. You should see

Now we have out ejs views setup it's time to add write some scripts to ejs templates.

Serving static content in express

Before adding some scripts we need to let express know where our static content is so it can serve it. Add this in the app.ts file.

const publicDirectoryPath = path.join(__dirname, "./public");
app.use(express.static(publicDirectoryPath));

So now we have a public directory set up now let's move ahead with using typescript with ejs.

Let's create 3 files inside public/ts directory and let's name them as scripts.ts, student.ts, professor.ts

professor.ts

export default class Professor {
    name: string;
    constructor(name: string){
        this.name = name
    }
}

student.ts

import Professor from "./professor";

export default class Student {
  name: string;
  favProfessor: Professor;
  constructor(name: string, professor: Professor) {
    this.name = name;
    this.favProfessor = professor;
  }

  getFavProfessor = () => {
     return this.favProfessor
  }

}

script.ts

import Professor from "./professor";
import Student from "./student";


const professor: Professor = new Professor("James Mathew")

const student: Student = new Student("Rohit Lakhotia", professor)

console.log(student.getFavProfessor())

So here, the file professor.ts contains the professor class and the student.ts file contains the student class with a method getFavProfessor which gets the student's favorite professor.

And the scripts.ts file imports both those files and output the student's favorite professor basically trying to import and export class/function which would happen in a real-world project.

Now since the script.ts file is the starting point we need to import that in the ejs template that we created before.

Browsers don't understand typescript. So we would need to transpile the typescript into javascript so the browser can understand.

Run the server using

npm run dev

Now if you see the dist/ts directory, and check out scripts.js file, you can see the converted javascript file. Now let's try running it.

But before that, we would need to make some changes. In the index.ejs just change the scripts.js location to take the script from ts directory. So it would like this.

<script src="./ts/scripts.js"></script>

Now run the server using

npm run dev

The server should start now and then now go to http://localhost:3000 and open the console in the browser and if you see exports is not defined.

Just add this before the script tag in index.ejs

<script>var exports = {}</script>

Now try running it. It would show a new error and that will be require is not defined

This is because if you see the transpile javascript file you would see the require function is there and the browser can't understand that

This is because when transpiling the code it was transpiled according to node which can work with require function.

So now what?

You can find the source code until here in the master branch on this repository


Webpack to the rescue

So webpack is a javascript bundler, basically, it converts your asset into single/multiple bundles so you can ship the bundle to the end-user basically reducing the number of HTTP calls made.

Webpack's working

We would be two methods for this to work

  1. Bundling the javascript generated into a single bundle
  2. Bundling the typescript directly into a bundle

Both the process would give the same output, but they would differ in the build times.

But before that, we need to install webpack and webpack-cli

npm i -D webpack webpack-cli

After this create a new webpack.config.js file in the root directory

Bundling the javascript generated into a single bundle

So here we will be generating the bundle from the already created files inside the dist/public/ts folder.

Bundling javascript with require statement to javascript without require statement using webpack

In the webpack.config.js file add this code

// The base directory that we want to use
const baseDirectory = "dist";

module.exports = {
  // The current mode, defaults to production
  mode: "development",

  // The entry points ("location to store": "location to find")
  entry: {
    "public/js/scripts": [`./${baseDirectory}/public/ts/scripts`],
    // "other output points" : ["other entry point"] 
  },

  // Used for generating source maps (used for debugging)
  devtool: "eval-source-map",

  // The location where bundle are stored
  output: {
    filename: "[name].js",
  },
};

Save the file and now we need to run the webpack command in order to bundle the javascript into a bundle, but this should be automated.

So now in package.json file, add a new script

"webpack": "webpack"

And we need to run this after the gulp script is done running, so after the gulp command, we need to add webpack. So the final scripts object would look like.

{
    "start:gulp": "npm-run-all gulp webpack start",
    "webpack": "webpack",
    "gulp": "gulp",
    "start": "node ./dist/app.js",
    "dev": "nodemon --watch src -e ts,ejs --exec npm run start:gulp"
}

Now before running npm run dev, just go to the index.ejs inside the src/views directory and make the scripts is loading from the js directory.

And then run

npm run dev

Now after the server is running successfully go to the http://localhost:3000 and check the console, you should see the Professor object consoled.

Congrats it's working now. Basically, you were able to use typescript in the browser using webpack.

Advantages:

  • Faster build time as compared to transpiling the frontend typescript directly to javascript using webpack

Disadvantages:

  • When you need to debug something in the browser console (since the source map we generated), you are directed to the javascript (the one with require functions), which are already transpiled from typescript which sometimes are not readable, but it works just fine.  

You can find the source code till here on the branch webpack-setup-through-javascript in the repository

Bundling the typescript directly into a bundle

Bundling typescript to javascript without require statement using webpack

So here rather than bundling the javascript generated through the gulp command, we would be using webpack modules to directly transpile the typescript into the javascript.

For that, we need to install ts-loader, it is a typescript loader for webpack.

npm i -D ts-loader

Now in the webpack.config.js file add his code

// The base directory that we want to use
const baseDirectory = "src";

module.exports = {
  // The current mode, defaults to production
  mode: "development",

  // The entry points ("location to store": "location to find")
  entry: {
    "public/js/scripts": [`./${baseDirectory}/public/ts/scripts`],
     // "other output points" : ["other entry point"] 
  },
  // Using the ts-loader module
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader",
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
  // Used for generating source maps (used for debugging)
  devtool: "eval-source-map",

  // The location where bundle are stored
  output: {
    filename: "[name].js",
  },
};

Things to notice here:

  1. The baseDirectory is src now
  2. A module and resolve object is added.

So now if we run this it would work, but now the public/ts directory is getting transpiled twice, one with the gulp task "typescript" and one with webpack.

So, to change that let's go the gulpfile.js, here on the typescript task, we need to exclude the public/ts directory since that would be done anyway by webpack.

tsProject.config['exclude'] = ["./src/public/ts/**/*"]

So the typescript task would look like this.

gulp.task("typescript", function () {
    tsProject.config['exclude'] = ["./src/public/ts/**/*"]
    return tsProject.src().pipe(tsProject()).js.pipe(gulp.dest("dist"));
});

Now if you run the server by npm run dev, you can access the browser console and see the Professor object

Congrats, you just made use of typescript for frontend.

Advantages:

  • While debugging the code in the browser console, you are directed to the typescript file (considering you have enabled the sourceMap: true in the tsconfig.json file), rather than the transpiled javascript file. This way you know exactly the line that caused the issue.

Disadvantages:

  • A little longer build time as compared to the previous method.

You can find the source code till here on the branch webpack-setup-through-typescript in the repository

What you can do is shift between the two when you find the required situation.

If you need any help, let me know in the comment section below