How to setup Nodejs, EJS with Typescript using Gulp and Webpack
Ever wanted to use typescript with nodejs for the backend and for the frontend using EJS templating engine? Let's use gulp and webpack for that.
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.
"moduleResolution": "node"
"rootDir": "./src"
"outDir": "./dist"
"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.
We would be two methods for this to work
- Bundling the javascript generated into a single bundle
- 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.
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
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:
- The
baseDirectory
issrc
now - 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