In Praise of TypeScript

Insights on making NodeJS APIs great

photo of Dmytro Zharkov
Dmytro Zharkov

Senior Software Engineer

Posted on Mar 22, 2018

Insights on making NodeJS APIs great

NodeJS is getting more and more popular these days. It’s gone through a long and painful history of mistakes and learning. By being a “window” for front-end developers to the “world of back-end,” it has improved the overall tech knowledge of each group of engineers by giving them the opportunity to write actual end-to-end solutions themselves using familiar approaches. It is still JavaScript, however, and that makes most back-end engineers nauseous when they see it. With this article and a number of suggestions, I would like to make NodeJS APIs look a bit better.

If you prefer looking at code over reading an article, jump to the sample project directly.

As a superset of JavaScript, TypeScript (TS) enhances ES6 inheritance with interfaces, access modifiers, abstract classes and methods (yeap, you read it correctly... abstract classes in JS), static properties, and brings strong typings. All of those can help us a lot. So, let’s walk through these cool features and check out how can we use them in NodeJS applications.

I split this post into two parts: an overview and actual code samples. If you know TS pretty well, you can jump to part two.

PART 1. OVERVIEW

INTERFACES, CLASSES, ABSTRACT CLASSES, AND TYPE ALIASES When I first tried TS, sometimes I felt like it went nuts checking and applying types. It’s technically possible to define variable type with type aliases, interfaces, classes and abstract classes so they really look pretty similar–kind of twins or quadruplets in this case–but as I looked into TypeScript more, I found that just like siblings they are actually really individual.

Interfaces are “virtual structures” that are never transpiled into JS. Interfaces are playing a double role in TS. They can be used to check if class implements certain patterns, and also as type definitions (so called “structural subtyping”).

I really like how TS allows us to extend interfaces so we can always modify already existing ones to our own needs.

Say we have a middleware function that performs some checks on request and adds additional property to requests named “supeheroName.” TS compiler will not allow you to add it on a standard express request, so we can extend this interface with needed property.

import { Request, Response } from  "express";
interface SuperHeroRequest extends Request {
superheroName: string;
}

And then use it in a route:

app.router.get("/heroes", (req: SuperHeroRequest, res: Response) => {
 if (req.superheroName) {
   res.send("I'm Batman")
 }
});

Of course, let’s not forget about the main function of interfaces; enforcing classes to meet a particular contract.

interface Villain {
 name: string;
 crimes: string[];
 performCrime(crimeName: string): void;
}
/* Compiler will ensure that all properties of IVillain interface are specified in implementing class and throw an errors on compile time if something is missing. */
class SuperVillain implements Villain {
 public name: string;
 public crimes: string[];

 constructor(name: string, crimes: string[] = []) {
   this.name = name;
   this.crimes = crimes;
 }

 performCrime(crime: string) {
   this.crimes.push(crime);
 }

 getCrimesList() {
   return this.crimes.join("\n");
 }
}



const doctorEvil = new SuperVillain("Doctor Evil");
doctorEvil.performCrime("Takeover the world");
doctorEvil.performCrime("Eat a donut");

console.log(doctorEvil.getCrimesList());

Abstract classes are usually used to define base level classes from which other classes may be derived.

abstract class Hero {
 constructor(public name: string, public _feats: string[]) {
}
 // Similar to interfaces we can specify method signature, that should be defined in derived classes.
 abstract performFeat(feat: string): void;
 // Unlike interfaces abstract classes can provide implementation along with method    signature.
 getFeatsList() {
   return this._feats.join("\n");
 }
}
class SuperHero extends Hero {
 constructor(name: string, _feats: string[] = []) {
   super(name, _feats);
 }
 performFeat(feat: string) {
   this._feats.push(feat);
   console.log(`I have just: ${feat}`);
 }
}

const Thor: SuperHero = new SuperHero("Thor", ["Stop Loki"]);
Thor.performFeat("Save the world");
console.log(Thor.getFeatsList());


// Abstract classes can be used as a type as well.

const Hulk: Hero = new SuperHero("Bruce Banner");
Hulk.performFeat("Smash aliens");
console.log(Hulk.getFeatsList());


// A try to instantiate abstract class will not work
const Loki: Hero = new Hero("Thor", ["Stop Loki"]);

As you can see, we can potentially use all of those by specifying a variable type. So what should be used and when? Let's sum it up.

null

Type aliases can be used to define primitive and reference types: string, number, boolean, object. You can’t extend type aliases.

Interfaces can define only reference (object) types. TS documentation recommends that we use interfaces for object type literals. Interfaces can be extended and can have multiple merged declarations, so users of your APIs may benefit from it. Interface is a “virtual” structure that never appears in compiled JavaScript.

Classes, as opposed to interfaces, not only check how an object looks but ensure concrete implementation as well.

Classes allow us to specify the access modifiers of their members.

The TS compiler always transpiles classes to actual JS code, so they should be used if an actual instance of the class is created. EcmaScript native classes can be also used as a type definitions.

let numbersOnly: RegExp = /[0-9]/g;
let name: String = "Jack";

Abstract classes are really a mix of the previous two, but as it’s not possible to instantiate them directly you can only use them as a type, if an instance is created from a derived class that doesn’t provide any additional methods or properties.

ACCESS MODIFIERS Unfortunately, JS doesn’t provide access modifiers so you can’t create, for example, a real private property. It’s possible to mock private property behaviour with closures and additional libraries, but such code looks a bit fuzzy and rather long. TS solves this issue just like any other Object Oriented Programming language. There are three access modifiers available in TS: public, private and protected.

PART 2. THE APPLICATION OR A DIVE INTO THE CODE.

So now, when we know and have all the tooling we need, we can build something great. For example, I would like to build a backend part of a MEAN (MongoDB, ExpresJS, Angular, NodeJS) stack; a simple RESTful service that will allow us to make CRUD operations with some articles. As including all the code will make this post too long, I’ll skip some parts, but you can always check the full version in the GitHub repository.

For project structure, see below:

null

To make code more declarative, easier to maintain and reusable, I’ll take advantage of ES6 classes and split the application into logical parts. I’m leaving most of the explanation in the comments.

./classes/Server.ts

import * as express from "express";
import * as http from "http";
import * as bodyParser from "body-parser";
import * as mongoose from "mongoose";
import * as dotenv from "dotenv";
import * as logger from "morgan";

/* Create a reusable server class that will bootstrap basic express application. */

 export class Server {

 /* Most of the core properties belove have their types defined by already existing interfaces. IDEs users can jump directly to interface definition by clicking on its name.  */

/* protected member will be accessible from deriving classes.  */
 protected app: express.Application;

 /* And here we are using http module Server class as a type. */
 protected server: http.Server;

 private db: mongoose.Connection;

 /* restrict member scope to Server class only */
 private routes: express.Router[] = [];
 /*  This could be done using generics like syntaxis. You can choose which is looking better for you
 private routes: Array = [];
*/

 /* public modifiers are default ones and could be omitted. I prefer to always set them, so code  style is more consistent. */
 public port: number;

 constructor(port: number = 3000) {
   this.app = express();
   this.port = port;
   this.app.set("port", port);
   this.config();
   this.database();
 }

 private config() {
  // set bodyParser middleware to get form data
   this.app.use(bodyParser.json());
   this.app.use(bodyParser.urlencoded({ extended: true }));
   // HTTP requests logger
   this.app.use(logger("dev"));
   this.server = http.createServer(this.app);

   if (!process.env.PRODUCTION) {
     dotenv.config({ path: ".env.dev" });
   }
 }

 /* A simple public method to add routes to the application. */
 public addRoute(routeUrl: string, routerHandler: express.Router): void {
   if (this.routes.indexOf(routerHandler) === -1) {
     this.routes.push();
     this.app.use(routeUrl, routerHandler);
   }
 }

 private database(): void {
   mongoose.connect(process.env.MONGODB_URI);
   this.db = mongoose.connection;
   this.db.once("open", () => {
     console.log("Database started");
   });
   mongoose.connection.on("error", () => {
     console.log("MongoDB connection error. Please make sure MongoDB is running.");
     process.exit();
   });
 }

 public start(): void {
   this.app.listen(this.app.get("port"), () => {
     console.log(("  App is running at http://localhost:%d in %s mode"), this.app.get("port"), this.app.get("env"));
     console.log("  Press CTRL-C to stop\n");
   });
 }
}

export default Server;

I have set the server and app properties to “protected” as I want to keep them private, so it’s not possible to override or access them directly. They could be reachable from derived classes. For example, if we want to add web sockets support to our server, we can extend it with a new class and use “server” or an “app” properties as we need.

./classes/SocketServer.ts

import Server from "./Server";
import * as io from "socket.io";

class SocketServer extends Server {

/* this.server of a parent Server class is protected property, so we can access it to add a socket.  */
 private socketServer = io(this.server);

 constructor(public port: number) {
   super(port);
   this.socketServer.on('connection', (client) => {
     console.log("New connection established");
   });

 }
}
export default SocketServer;

Going back to the application.

./app.ts

import Server from "./classes/Server";
import ArticlesRoute from "./routes/Articles.route";

const app = new Server(8080);
const articles = new ArticlesRoute();
app.addRoute("/articles", articles.router);
app.start();

As we can have multiple kinds of articles (products) e.g. electronic, fashion, digital, etc. and they might have rather different sets of properties, I’ll create a base abstract class with a number of default properties that should be common for all types of articles. All other properties can be defined in derived classes.

./classes/AbstractArticle.ts

// put basic properties into abstract class.

import ArticleType from "../enums/ArticleType";
import BaseArticle from "../interfaces/BaseArticle";
import * as uuid from "uuid";
import Price from "../interfaces/IPrice";

abstract class AbstractActrticle implements BaseArticle {
 public SKU: string;
 constructor(public name: string, public type: ArticleType, public price: Price, SKU: string) {
   this.SKU = SKU ? SKU : uuid.v4();
 }
}

export default AbstractActrticle;

For this example, I’ll create a Shoe class that will derive from an AbstractArticle class and set its own properties.

./classes/Shoe.ts

import AbstractActrticle from "./AbstractArticle";
import ArticleType from "../enums/ArticleType";
import Colors from "../enums/Colors";
import FashionArticle from "../interfaces/FashionArticle";
import Price from "../interfaces/Price";
import Sizes from "../enums/Sizes";

class Shoe extends AbstractActrticle implements FashionArticle {
 constructor(public name: string,
             public type: ArticleType,
             public size: Sizes,
             public color: Colors,
             public price: Price,
             SKU: string = "") {
   super(name, type, price, SKU);
 }
}

export default Shoe;

You might have noticed that Shoe class implements FashionArticle interface. Let’s take a look at it and see how we can benefit from Interfaces and possibility to extend those.

./interfaces/BaseArticle.ts

import ArticleType from "../enums/ArticleType";
import Price from "./Price";

interface BaseArticle {
 SKU: string;
 name: string;
 type: ArticleType;
 price: Price;
}

Extension of interfaces allows us to extend our own interfaces with additional properties.

./interfaces/FashionArticle.ts

import Colors from "../enums/Colors";
import BaseArticle from "./BaseArticle";
import Sizes from "../enums/Sizes";

interface FashionArticle extends BaseArticle {
 size: Sizes;
 color: Colors;
}

We can also extend already existing interfaces. As an example, I’ll create an FashioArticleModel interface that will extend the Document interface from Mongoose and our  FashionArticle interface so we can use it when creating database schema.

./interfaces/FashionArticleModel.ts

import { Document } from "mongoose";
import FashionArticle from "./FashionArticle";

interface FashionArticleModel extends FashionArticle, Document {};
export default FashionArticleModel;

Using IFasionArticleModel interface in the schema allows us to create a model with properties from both the Mongoose Document and FashionArticle interfaces.

./schemas/FashionArticle.schema.ts

import { Schema, Model, model} from "mongoose";
import FashionArticleModel from "../interfaces/FashionArticleModel";

const ArticleSchema: Schema = new Schema({
 name: String,
 type: Number,
 size: String,
 color: Number,
 price: {
   price: Number,
   basePrice: Number
 },
 SKU: String
});

// Use Model generic from mongoose to create a model of FashionArticle type.
const ArticleModel: Model = model("Article", ArticleSchema);
export {ArticleModel};

I hope this example application already shows how TypeScript can make your code more declarative, self documentable and potentially easier to maintain. Using TS is also a good exercise for frontend developers to learn and apply OOP paradigms in real life projects, and backend developers should find many familiar practices and code constructs.

Finally I would suggest to jump into Articles route class and check a CRUD functionality of the application.

./routes/Articles.route.ts

import { Request, Response, Router } from "express";
import ArticleType from "../enums/ArticleType";
import Colors from "../enums/Colors";
import Shoe from "../classes/Shoe";
import Sizes from "../enums/Sizes";
import { ArticleModel } from "../schemas/FashionArticle.schema";
import FashionArticleModel from "../interfaces/FashionArticleModel";

class ArticlesRoute {
 public router: Router;

 constructor() {
   this.router = Router();
   this.init();
 }

 // Putting all routes into one place makes it easy to search for specific functionality
 // As this method will be called in a context of a different class, we need to bind methods objects to current class.
 public init() {
   this.router.route("/")
     .get(this.getArticles.bind(this))=
     .post(this.createArticle.bind(this));

   this.router.route("/:id")
     .get(this.getArticleById.bind(this))
     .put(this.updateArticle.bind(this))
     .delete(this.deleteArticle.bind(this));
 }
 // I'm not a huge fan of JavaScript callbacks hell and especially of using it in NodeJS, so I'll use promises   instead.
 public getArticles(request: Request, response: Response): void {
   ArticleModel.find()
     .then((articles: FashionArticleModel[]) => {
       return response.json(articles);
     })
     .catch((errror: Error) => {
       console.error(errror);
     })
 }

 public getArticleById(request: Request, response: Response): void {
   const id = request.params.id;
   ArticleModel
     .findById(id)
     .then((article: FashionArticleModel) => {
     return response.json(article);
   })
     .catch((error: Error) => {
       console.error(error);
       return response.status(400).json({ error: error });
   });
 }

 public createArticle(request: Request, response: Response): void {
   const requestBody = request.body;
   const article = new Shoe(requestBody.name, requestBody.type, requestBody.size, requestBody.color, requestBody.price);

   const articeModel = new ArticleModel({
     name:  article.name,
     type:  article.type,
     size:  article.size,
     color: article.color,
     price: article.price,
     SKU:   article.SKU
   });

   articeModel
     .save()
     .then((createdArticle: FashionArticleModel) => {
       return response.json(createdArticle);
     })
     .catch((error: Error) => {
       console.error(error);
       return response.status(400).json({ error: error });
     });
 }

 public updateArticle(request: Request, response: Response): void {
   const id = request.params.id;
   const requestBody = request.body;
   const article = new FashionArticle(requestBody.name, requestBody.type, requestBody.size, requestBody.color, requestBody.price, requestBody.SKU);

   ArticleModel.findByIdAndUpdate(id, article)
     .then((updatedArticle: FashionArticleModel) => {
       return response.json(updatedArticle);
     })
     .catch((error: Error) => {
       console.error(error);
       return response.json({ err: error });
     })
 }

 public deleteArticle(request: Request, response: Response): void {
   const articleId = request.params.id;
    ArticleModel.findByIdAndRemove(articleId)
     .then((res: any) => {
       return response.status(204).end();
     })
     .catch((error: Error) => {
       console.error(error);
       return response.json({ error: error });
     });
 }
}
export default ArticlesRoute;

As a conclusion, TypeScript is a powerful tool that brings a really flexible, reach type checking system to your code. It also introduces enhanced well-known patterns like interfaces, abstract classes and access modifiers.

Of course, the application is not ready for production use, as we have to cover everything with tests and set up a proper development environment, but we can cover that in the future.


We're hiring! Do you like working in an ever evolving organization such as Zalando? Consider joining our teams as a Frontend Engineer!



Related posts