If you have worked with a framework like Angular you have used syntax such as @Component, @HostListenerand and @viewchild. These are known as TypeScript decorators. They are one of the most powerful yet under-used features of TypeScript.

In this article we will discuss:

  • What decorators are
  • How to implement your own decorators
  • Leveraging decorators to write clean modular code
  • Aspect Oriented Programming and decorators
  • Cross-cutting-concerns and solving them with decorators

First - What is a Decorator?

A decorator is a function that allows us to annotate our code or hook into its behavior (similar to meta-programming).

Decorators are very good at creating abstractions. When a piece of logic needs to be duplicated in many places we can use a decorator for it. Decorators also solve problems that are hard to solve with inheritance (more on this later).

There are 4 types of Decorators in TypeScript and they are as follows:

Class Decorators

A Class Decorator is declared before a class declaration. A class decorator is applied to the constructor of the class and can be used to observe, modify, or replace a class definition.

Here’s an example:

@robot
class Greeter {
 private greeting: string;
 constructor(message: string) {
   this.greeting = message;
 }
 greet() {
   return "Hello, " + this.greeting;
 }
}
 
function robot<T extends { new (...args: any[]): {} }>(constructor: T) {
 return class extends constructor {
   greeting = "from  Robot :-{";
 };
}
let k = new Greeter("shadid");
console.log(k.greet());

In this example we are overriding the constructor of our Greeter class. Our robot decorator function is just like a normal function. However, it takes in a class constructor as an argument. We can use this constructor that is being passed in as a parameter and override its functionality.

Method Decorator

A Method Decorator is declared before a method declaration. It is applied to the Property Descriptor for the method, and can be used to observe, modify, or replace a method definition.

Here’s an example:

declare type MethodDecorator = <T>(
 target: Object,
 propertyKey: string | symbol,
 descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
 
class Robot {
 greeting: string;
 constructor(message: string) {
   this.greeting = message;
 }
 
 @logMethod("Greet")
 greet() {
   return "Hello, " + this.greeting;
 }
}
 
function logMethod(message: string): MethodDecorator {
 console.log(`${message} evaluated`);
 return function(
   target: Object,
   propertyKey: string,
   descriptor: PropertyDescriptor
 ): void {
   console.log(`${message} called`);
 };
}
 
let bot = new Robot("I am robo, nice to meet you :)");
console.log(bot.greet());

Here we first defined a MethodDecorator type. We created a method decorator called logMethod. This decorator function is a factory function that returns another function of type MethodDecorator. While using decorators it is a common practice to use a factory function. The returned function is where we will usually apply our logic.

Property Decorator

The Property Decorator is applied to a Property definition. You are probably noticing a pattern already 😊. Below is an example:

class Greeter {
 @logGreet("Logging Greetings")
 greeting: string;
 ....
}
 
function logGreet(formatString: string) {
 console.log(formatString);
}

Parameter Decorator

As the name suggests, this decorator is applied to the parameters of methods. Below is an example:

class Greeter {
 ...
 public makeRobotNoise(
   @logParameter("Param for method robot noise") person: Person
 ) {
   this._directReports.push(person);
 }
}
 
function logParameter(message: string): ParameterDecorator {
 console.log(`${message} logParam factory`);
 return function(
   target: Object,
   propertyKey: string,
   parameterIndex: number
 ): void {
   console.log(`${message} called from return function`);
 };
}

These are some of the basic examples of decorators. Now let's work though a real world project together where we can implement decorators to cut down repeated functionality in our code.

Applying what we've learned

How to implement your own decorators

We will be building a REST api server with Node.js, Express and TypeScript. We will structure our code for scalability and proper testing. We will also look at a concept called Aspect Oriented Programming (AOS) and apply a layered architecture.

Alright let's dive in.

Get the starter code from the following directory. I have setup a TypeScript project with Express for you to follow along.

https://github.com/Shadid12/entity-framework/tree/intro_1

Let's go over the project together.

src/app.ts

import APIServer from "./APIServer";
 
export const apiServer = new APIServer();
 
apiServer.start();

app.ts is our application entry point. You can see that we are creating an instance of the APIServer class and calling the start() method. APIServer class encapsulates all our server related code. Let's take a look inside of it.

import * as http from "http";
import express, { Express } from "express";
import bodyParser from "body-parser";
 
export default class APIServer {
    private _app: Express;
    private _server: http.Server;
 
 
    constructor() {
        this._app = express();
 
        // Set port
        this._app.set("port", process.env.PORT || 3000);
 
        // Add Middleware
        this.configureMiddleware();
    }
 
 }

First we import all the necessary libraries such as express, bodyParser and http. Then we create the class. We also declare two private variables _app and _server. In our constructor we are creating the express instance and setting it to _app. We are also calling the configuration method. This method will take care of the express configuration code.

public configureMiddleware() {
        // Setup body parsing - required for POST requests
        this._app.use(bodyParser.json());
        this._app.use(bodyParser.urlencoded({ extended: true }));
 
        // Setup CORS
        this.app.use(function (req, res, next) {
            res.setHeader("Access-Control-Allow-Origin", "*");
            res.setHeader("Access-Control-Allow-Credentials", "true");
            res.setHeader("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS,POST,PUT");
            res.setHeader("Access-Control-Allow-Headers", "Access-Control-Allow-Origin,Access-Control-Allow-Headers, Origin,Accept, X-Requested-With, Content-Type, Access-Control-Request-Method, Access-Control-Request-Headers,Authorization");
            next();
        });
    }

In addition to this we also set up two getters to get our express and server instances.

     /**
     * Getters
     */
 
    get app(): Express {
        return this._app;
    }
 
    get server(): http.Server {
        return this._server;
    }

Finally we have the start method that starts our express server.

/**
     * Public Methods
     */
    public start() {
        // Start the server instance
        this._server = this._app.listen(this._app.get("port"), () => {
            console.log("Server is running on port " + this._app.get("port"));
        });
    }

So far so good 👍. Next, we know that we would like to do CRUD (Create, Read, Update, Delete) operations with our API.

Leveraging decorators to write clean modular code

Let’s say we need Books and Authors resources in our API. So normally we would create book routes, book model and a book controller to handle the CRUD operations for books. Then we would repeat the same process for Authors.

But let’s say in the future we add more resources such as Albums, Artists and Movies. We would have to repeat the same process of creating routes, controllers and models for each resource.

But what if we could avoid repeating this code logic? That’s exactly what we are going to do with decorators.

You can find the complete code for this section in the following link:

https://github.com/Shadid12/entity-framework/tree/setting_routes_02

Let’s revisit our app.ts file.

import APIServer from "./APIServer";
import { JsonDB } from "node-json-db";
import { Config } from "node-json-db/dist/lib/JsonDBConfig"
import Author from "./entities/Author";
 
export const apiServer = new APIServer();
 
export const db = new JsonDB(new Config("DB", true, true, '/'));
 
apiServer.start();

As you can see I have added a couple extra things in this file. First of all I added JsonDB as our database, which is what I will be using (however, feel free to use a database of your choice).

Next we create the file ./entities/Author.ts. Then we import Author class into our app.ts. Let’s take a look at the Author class.

import BaseEntity from './BaseEntity';
import { entity } from '../decorators/entity';
 
@entity("author")
export default class Author extends BaseEntity {
 
    id: string;
 
    firstName: string;
 
    lastName: string;
 
    email: string;
 
    alias: string;
}

As you can see our class extends a BaseEntity class. Our BaseEntity class doesn’t really have any functionality yet. However, we will implement it as we go. Here’s what it looks like:

export default class BaseEntity implements IEntity {
 
    getPersistenceObject(): any {
        // TODO: Implement this
    }
}

Now let’s go back to the Author class and you will notice that we have defined a class decorator called entity. Let’s take a look into the decorators/entity.ts file and see what’s happening.

import "reflect-metadata";
 
export function entity(name: string) {
    return function (constructor: Function) {
        Reflect.defineMetadata("entity:name", name, constructor);
    }
}

We are creating a simple decorator factory here. Inside the function we are using the defineMetadata API from TypeScript to pass in the resource name. We will be using this metadata to dynamically generate routes for all of our resources.

Let’s go back to our apiServer class and add another method called addEntity.

import * as http from "http";
import express, { Express } from "express";
import bodyParser from "body-parser";
import BaseEntity from "./entities/BaseEntity";
import EntityRouter from "./EntityRouter";
 
export default class APIServer {
    … 
    public addEntity<T extends BaseEntity>(clazz) {
       const name = Reflect.getMetadata("entity:name", clazz);
       let entityRouter = new EntityRouter<T>(name, clazz);
       this._app.use(`/${name}`, entityRouter.router);
    }
}

In this method we are accepting a class as a parameter. We are then using the Reflect.getMetadata(); to get the resource name. This is why we created the @entity decorator. We pass in this name to our EntityRouter class to create the dynamic routes.

Let’s take a look at the EntittyRouter class.

import BaseEntity, { EntityTypeInstance } from "./entities/BaseEntity";
import express, { Router, Request, Response } from "express";
import { db } from "./app";
 
export default class EntityRouter<T extends BaseEntity> {
 
    private _router: Router;
 
    get router(): Router {
        return this._router;
    }
 
    constructor(public name: string, private classRef: EntityTypeInstance<T>) {
        this._router = express.Router();
        this.addEntityRoutes();
    }
 
    addEntityRoutes() {
        // CREATE
        this._router.post('/', (req, res) => {
            this.createEntity(req, res);
        });
 
        // READ all
        this._router.get('/', (req, res) => {
            this.fetchAllEntities(req, res);
        });
 
        // READ one
        this._router.get('/:id', (req, res) => {
            this.fetchEntity(req, res);
        });
 
        // UPDATE
        this._router.put('/:id', (req, res) => {
            this.updateEntity(req, res);
        });
 
        // DELETE
        this._router.delete('/:id', (req, res) => {
            this.deleteEntity(req, res);
        });
    }
 
    private fetchAllEntities(req: Request, res: Response) {
        let data = {}
        data = db.getData(`/${this.name}`);
        res.json(data);
    }
 
    private fetchEntity(req: Request, res: Response) {
        let data = {}
        data = db.getData(`/${this.name}/${req.params.id}`);
        res.json(data);
    }
 
    private createEntity(req: Request, res: Response) {
        // TODO: Implement
    }
 
    private updateEntity(req: Request, res: Response) {
        // TODO: Implement
    }
 
    private deleteEntity(req: Request, res: Response) {
        db.delete(`/${this.name}/${req.params.id}`);
        res.json({});
    }
 
}

Here we have a constructor that is accepting the resource name and a class reference for our resource. We have also setup the routes/CRUD operations to be dynamic. Now finally in our app.ts we have to add this resource in.

...
import Author from "./entities/Author";
 
export const apiServer = new APIServer();
 
export const db = new JsonDB(new Config("DB", true, true, '/'));
+apiServer.addEntity<Author>(Author);
 
apiServer.start();

Now let’s create some test data to test our API. Create a file called ./DB.json and add the following records.

{
    "author": {
        "8a91a0f4-39c9-4d54-aff0-b636a667c560": {
            "id": "8a91a0f4-39c9-4d54-aff0-b636a667c560",
            "firstName": "Steven",
            "lastName": "Hawking",
            "email": "steve@gmail.com",
            "alias": "sh"
        },
        "18583e4a-1e4e-487b-9004-13b195ec42bc": {
            "id": "18583e4a-1e4e-487b-9004-13b195ec42bc",
            "firstName": "Steven",
            "lastName": "Hawking",
            "email": "steve@gmail.com",
            "alias": "sh"
        }
    },
    "book": {
        "349a7c69-75ac-45e1-b423-88172829df60": {
            "id": "349a7c69-75ac-45e1-b423-88172829df60",
            "title": "A brief History of Time",
            "author": "Steven Hawking",
            "authorId": "2020202020"
        }
    }
}

Now if you do a GET request http://localhost:3000/author you will get a list of authors. Also doing GET http://localhost:3000/author/18583e4a-1e4e-487b-9004-13b195ec42bc should give you the specific user.

Now if we add another resource, for instance a book resource, all we have to do is define our book entity and add it to api.ts. Let’s do that now.

import APIServer from "./APIServer";
import { JsonDB } from "node-json-db";
import { Config } from "node-json-db/dist/lib/JsonDBConfig"
import Author from "./entities/Author";
+import Book from "./entities/Book";
 
export const apiServer = new APIServer();
 
export const db = new JsonDB(new Config("DB", true, true, '/'));
apiServer.addEntity<Author>(Author);
+apiServer.addEntity<Book>(Book);
 
apiServer.start();

And make a Book class:

import BaseEntity from './BaseEntity';
import { entity } from '../decorators/entity';
 
@entity("book")
export default class Book extends BaseEntity {
    id: string;
 
    title: string;
 
    author: string;
 
    authorId: string;
}

And that’s it. Your Book resource now will work as expected.

We have a read operation for both of our resources. Now let’s implement the Create, Update and Delete as well. Let’s go back to our Author class and couple new decorators.

import BaseEntity from './BaseEntity';
import { entity, id, persist } from '../decorators/entity';
 
@entity("author")
export default class Author extends BaseEntity {
 
    @id
    id: string;
 
    @persist
    firstName: string;
 
    @persist
    lastName: string;
 
    @persist
    email: string;
 
    @persist
    alias: string;
}

In our entity.ts we will add these new decorators like below.

export function persist(target: any, propertyKey: string) {
    let objectProperties: string[] = Reflect.getMetadata("entity:properties", target) || [];
    if (!objectProperties.includes(propertyKey)) {
        objectProperties.push(propertyKey);
        Reflect.defineMetadata("entity:properties", objectProperties, target);
    }
}
 
export function id(target: any, propertyKey: string) {
    Reflect.defineMetadata("entity:id", propertyKey, target);
}

The persists decorator makes sure a certain key is available in the entity in order to save. The id decorator will be used for the uniqueId key for that object. Let’s go back to our EntityRouter class and populate the createEntity function.

private createEntity(req: Request, res: Response) {
        let newEntity = EntityFactory.fromPersistenceObject<T>(req.body, this.classRef);
 
        const idProperty = Reflect.getMetadata("entity:id", newEntity);
        newEntity[idProperty] = uuid.v4();
        db.push(`/${this.name}/${newEntity[idProperty]}`, newEntity.getPersistenceObject());
        res.status(200).json(newEntity);
    }

In the code above we are creating a new entity instance with method fromPersistenceObject. Here’s the implementation for EntityFactory.

export class EntityFactory {
 
    static fromPersistenceObject<T extends IEntity>(obj: Object, type: EntityTypeInstance<T>): T {
        let output = new type();
        const persistedProperties: string[] = Reflect.getMetadata("entity:properties", output) || [];
        const idProperty = Reflect.getMetadata("entity:id", output);
        const props = Object.keys(obj);
        for (const prop of props) {
            if (persistedProperties.includes(prop) || prop == idProperty) {
                output[prop] = obj[prop];
            } else {
                throw new Error("Property not defined in class.");
            }
        }
        return output;
    }
}

In this method we are accepting request object and entity type as parameters. We are making sure that request object properties exist in our class definition. If a property doesn't exist it will throw an error. Now let’s implement the getPersistenceObject in our BaseEntity class. This method returns an object with the properties and value of an entity.

export default class BaseEntity implements IEntity {
 
    getPersistenceObject(): any {
        let output = {};
        const persistedProperties = Reflect.getMetadata("entity:properties", this);
        const idProperty = Reflect.getMetadata("entity:id", this);
        output[idProperty] = this[idProperty];
        for (const prop of persistedProperties) {
            if (this[prop]) {
                output[prop] = this[prop];
            }
        }
        return output;
    }
 
}

Let’s also implement the update and delete methods for our entity. Let’s add the following methods to our EntityRouter class.

private updateEntity(req: Request, res: Response) {
        // Does entity exist with ID
        let data = {}
        try {
            data = db.getData(`/${this.name}/${req.params.id}`);
        } catch (err) {
            res.status(404).json({ error: "Object does not exist" });
            return;
        }
 
        // Update Object with new values
        let updatedData = req.body;
        let updatedObj = EntityFactory.fromPersistenceObject(data, this.classRef);
        const propKeys = Object.keys(updatedData);
        for (const propKey of propKeys) {
            updatedObj[propKey] = updatedData[propKey];
        }
 
        // Save and Return data
        db.push(`/${this.name}/${req.params.id}`, updatedData, false);
        data = db.getData(`/${this.name}/${req.params.id}`);
        res.json(data);
    }
 
    private deleteEntity(req: Request, res: Response) {
        db.delete(`/${this.name}/${req.params.id}`);
        res.json({});
    }

That’s all. Our CRUD now should be fully operational. Feel free to checkout the code for this project up to this point in the following link.

https://github.com/Shadid12/entity-framework/tree/setting_routes_02

Feel free to create a couple of records and observe them in the database. For our sample app it is just getting stored in the DB.json file.

Cross cutting concerns

When architecting enterprise applications we often break them into multiple layers. For example if we are building a REST API we can have the following layers

  1. Service Layer: This is responsible for communicating between our API and different client side applications.
  2. Business Rules and Logic Layer: This is where we perform our business rules and logic based on the incoming request data.
  3. Data Access and Storage: This layer deals with data access, reading and writing data in our database.

We would like to keep these layers separate, but there will be some code logic that will apply to all of them. For example we would want to utilize authentication logic, validation logic or logging logic among every layer of our code.

In software design these are sometimes called horizontal dependencies. Here’s an image to visualize this concept.

A visualization of layered application
A visualization of layered application 

This dependency problem is known as Cross Cutting Concern. We can apply decorators to solve these types of problems. The paradigm of using decorators to solve these types of problems is called aspect oriented programming (AOP).

AOP is a programming paradigm that focuses on increasing modularity by separating cross cutting concerns.

Aspect Oriented Programming and decorators

In our application we want to apply AOP paradigm and separate cross cutting logic such as validation and authorization. Let’s see how we can implement validation logic to our code using decorators. The complete code for this part is available in the following github link.

https://github.com/Shadid12/entity-framework/tree/validators_03

Let’s go over the data validation code. We want to make sure that our Actor model has a proper email and certain fields are not empty. Let’s create these two additional decorator functions required and isEmail.

In our decorator folder we create a new file called validators.ts.

export function required(target: any, propertyKey: string) {
    addValidation(target, propertyKey, requiredValidatior);
}
 
export function isEmail(target: any, propertyKey: string) {
    addValidation(target, propertyKey, emailValidatior);
}

We created the decorator functions isEmail() and required(). We are passing in the target object and the propertyKey in these functions. Both of these functions then call the addValidation() with target, propertyKey and validation function.

requiredValidatior() is the function that is responsible for encapsulating the code that makes sure that property must be included and emailValidatior() contains the logic to check if the email is valid.

Here’s how we implement these functions:

import validator from "validator";
 
….
 
function requiredValidatior(target: any, propertyKey: string): string | void {
    let value = target[propertyKey];
    if (value) {
        return;
    }
    return `Property ${propertyKey} is required.`
}
 
function emailValidatior(target: any, propertyKey: string): string | void {
    let value = target[propertyKey];
    if (value == null) {
        return;
    }
    const isValid = validator.isEmail(value);
    if (!isValid) {
        return `Property ${propertyKey} must be a valid email.`
    }
    return;
}
Side Note: I am using a npm package called validator to check if email is valid.

Finally we have our addValidation() method. Let’s dissect this method:

function addValidation(target: any, propertyKey: string, validator: ValidationFunction, validationOptions?: any) {
    // Make sure we have the list of all properties for the object
    let objectProperties: string[] = Reflect.getMetadata("validation:properties", target) || [];
    if (!objectProperties.includes(propertyKey)) {
        objectProperties.push(propertyKey);
        Reflect.defineMetadata("validation:properties", objectProperties, target);
    }
 
    // Make sure we capture validation rule
    let validators: ValidationRule[] = Reflect.getMetadata("validation:rules", target, propertyKey) || [];
    let validationRule = {
        validator: validator,
        validationOptions: validationOptions
    };
    validators.push(validationRule);
    Reflect.defineMetadata("validation:rules", validators, target, propertyKey);
}

This method takes in target, propertyKey, and validation function as params. First it checks if those properties exist in the object. Then it adds all the validators into an array. A single property can have multiple validators, that’s why we have this array. Finally it defines these metadata with reflection

Now if we go back to our Author class and add these decorators to certain properties we will be able to capture those validation behaviors from any part of our code.

For this example let’s take a look at the Author class again.

@entity("author")
export default class Author extends BaseEntity {
 
    @id
    id: string;
 
    @persist
    @required
    firstName: string;
 
    @persist
    lastName: string;
 
    @persist
    @isEmail
    @required
    email: string;
 
    @persist
    alias: string;
}

As you can see in the code above some of our properties now have additional decorators for data validation.

Now let’s say we want to validate our data from the service layer. In our little application the EntityRouter class is the barebones service layer.

Now let’s say we want to validate data before we save it. We can achieve that through the following code:

EntityRoutes.ts

private createEntity(req: Request, res: Response) {
        let newEntity = EntityFactory.fromPersistenceObject<T>(req.body, this.classRef);
        
        let errorMap = validate(newEntity);
        if (Object.keys(errorMap).length > 0) {
            const output = { errors: errorMap };
            res.status(400).json(output);
            return;
        }
 
        const idProperty = Reflect.getMetadata("entity:id", newEntity);
        newEntity[idProperty] = uuid.v4();
        db.push(`/${this.name}/${newEntity[idProperty]}`, newEntity.getPersistenceObject());
        res.status(200).json(newEntity);
    }

Validators.ts

export function validate(object: any) {
    const keys = Reflect.getMetadata("validation:properties", object) as string[];
    let errorMap = {};
 
    if (!keys || !Array.isArray(keys)) {
        return errorMap;
    }
 
    for (const key of keys) {
        const rules: ValidationRule[] = Reflect.getMetadata("validation:rules", object, key) as ValidationRule[];
        if (!Array.isArray(rules)) {
            continue;
        }
        for (const rule of rules) {
            const errorMessage = rule.validator(object, key, rule.validationOptions);
            if (errorMessage) {
                errorMap[key] = errorMap[key] || [];
                errorMap[key].push(errorMessage);
            }
        }
    }
 
    return errorMap;
}

The validate() function itself invokes all the validator functions. You can see the benefits of using these decorators now. We have modularized our validation logic completely. We have complete separation of concerns. Under the hood, the Angular framework also uses decorators similar to our validators.

We can also use decorators to implement authorization/logging logic and handle cross cutting concerns. I have implemented a simple authentication logging to the project. If you are interested you can take a look at the code in the following link.

https://github.com/Shadid12/entity-framework

And that's a wrap!

I hope this article provided you with a good understanding of decorators.

We talked about how decorators can help us cut down repeated code. We looked at examples of defining metadata and built our own decorators similar to those used in popular frameworks such as Angular. We also looked at how to address cross cutting concerns in larger applications.

I hope you enjoyed this article, feel free to leave your valuable feedback. Until next time 😊