Setting Up a Node & TypeScript Project - Setup DB & ORM


We have our base project setup with Express, but what about connecting to a databse? We could just use raw SQL queries and that might be a good option for some project. Or we could use an ORM. An ORM, or Object Relational Mapper, is a library/framework that allows for the bi-directional mapping of your OOP model classes to relational database tables. It basically abstracts away SQL querying and result set mapping to our TypeScript models into a library to simplify our data access layer.

The two big ORM’s in Node are Sequelize and TypeORM with the later being the newest of the two. We’re going to use TypeORM. TypeORM supports both DataMapper and ActiveRecord patterns. Since I come from a Hibernate/Entity Framework background, we’re going to setup our project with the DataMapper pattern but if you prefer the ActiveRecord approach like Eloquent/Larvel, feel free make changes where necessary, TypeORM has excellent documentation on how to setup both.

We are also going to be using Sqlite for our project simply because it’s quick and easy to setup.

 

Prerequisites

 

Installing Our Dependencies

First we need to install our dependencies as usual. There are only a few we are going to need: typeorm, reflect-metadata, sqlite3.

npm install --save typeorm reflect-metadata sqlite3

The typeorm package is the core TypeORM library. We’ll also need reflect-metatdata for our TypeORM decorators. And of course sqlite3 for our Sqlite database.

 

Configuring TypeORM

TypeORM supports many configuration file formats include .env files so if you wish to do so, you can configure TypeORM in your already existing .env file. For our project we’re going to create a ormconfig.json file for our TypeORM configuration. Create your ormconfig.json file at the root of the project touch ormconfig.json with the following json data…

{
  "type": "sqlite",
  "database": "database.sqlite",
  "synchronize": true,
  "logging": false,
  "entities": [
    "src/app/data/entities/**/*.entity.ts"
  ],
  "migrations": [
    "src/app/data/migrations/**/*.ts"
  ],
  "subscribers": [
    "src/app/data/subscribers/**/*.subscriber.ts"
  ],
  "cli": {
    "entitiesDir": "src/app/data/entities",
    "migrationsDir": "src/app/data/migrations",
    "subscribersDir": "src/app/data/subscribers"
  }
}

Your ormconfig.json contains your connection details as well has where your entities, migrations, and subscribers are located in your project. Now we can create our data directories and barrel file…

# Create directories
mkdir src/app/data \
  src/app/data/migrations \
  src/app/data/entities \
  src/app/data/subscribers

# Create our data directory barrel file
touch src/app/data/index.ts

TypeORM also requires us to add three additional properties to our tsconfig.json file. Open your tsconfig and add strictPropertyInitialization, experimentalDecorators, and emitDecoratorMetadata. These properties are needed for our entity classes in order to use the TypeORM decorators.

{
  "compilerOptions": {
    "target": "es6",                         
    "module": "commonjs",                    
    "strict": true,                          
    "esModuleInterop": true,                  
    "noImplicitAny": true,
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "baseUrl": ".",
    "forceConsistentCasingInFileNames": true, 
    "strictPropertyInitialization": false,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "paths": {
      "*": [
        "node_modules/*"
      ]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

We also can add a couple scripts to our package.json file to help us create and run our migrations. Create two scripts, migrations:gen and migrations:run

{
  ...
  "scripts": {
    "test": "jest",
    "coverage": "jest --coverage",
    "lint": "eslint \"{src,test}/**/*.{js,ts}\"",
    "lint-fix": "npm run lint -- --fix",
    "migrations:gen": "ts-node ./node_modules/.bin/typeorm migration:generate",
    "migrations:run": "ts-node ./node_modules/.bin/typeorm migration:run",
    "start": "ts-node src/index.ts",
    "build": "tsc"
  },
  ...
}

And finally we will need to initialize our database connection in our entry point file, src/index.ts, by creating our database connection pool with TypeORMS’s createConnection function

import 'reflect-metadata';
import { createConnection } from 'typeorm';
import { Application } from './app';
import * as dotenv from 'dotenv';

dotenv.config();

createConnection().then(async () => {
  const application: Application = new Application();
  application.startServer();
});

 

Creating An Entity Model

There is serveral approaches for database to entity mapping. One way is creating and mapping your entities based on an already existing database also known as a database first approach. The other approach is called code first where you create your entities first and a database schema is generated based on those entity classes (this is where migrations will come in to play). Since we don’t have an existing database, we are going to go with the code first approach and let TypeORM create our databse based on our entities.

Expanding on our previous post, we’re going to create a User entity. We will use inheritance by creating an abstract base class which all of our entities will extend. The abstract base class will hold all common attribute/columns shared by all of our entities to keep our entities DRY. Since all tables in a relational database required a primary key, our abstract base class will hold a unique id (primary key) that all entities will require. You could also include things like creation timestamps in this base class as well.

Create our abstract base class with `touch src/app/data/entities/base.entity.ts

import { PrimaryGeneratedColumn } from 'typeorm';

export abstract class BaseEntity {
  @PrimaryGeneratedColumn()
  public id: number;
}

Now create our User entity, touch src/app/data/entities/user.entity.ts

import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
import { BaseEntity } from './base.entity';

@Entity()
export class User extends BaseEntity {
  @Column({ unique: true })
  public username: string;

  @Column()
  public fistname: string;

  @Column()
  public lastname: string;
}

Export our entity in our data/index.ts barrel file…

export * from './entities/user.entity.ts'; 

 

Creating a Migration

If you aren’t familiar with migrations, you can think of a migration as a way to keep track of changes to a database schema. A migration is created based on our entity classes. If we make a change to an entity class, we can create a migration which will contain all the SQL statements that need to run in order to keep our entity models and database schema in sync. You can kinda of think of a migration as a version control for a database schema in a way. Lets create our first migration.

We can use the script we added to our package.json file to create our first migration. The -n in the command will allow us to name our migration.

npm run migration:gen -- -n "generate-and-seed-users-table"

After generating the migration, you will see that a database.sqlite file was create at the root of our project. We will need to add this file to our `.gitignore since we do not want to commit our database to our repository.

dist/
node_modules/
coverage/
*.sqlite

Also if you check your migrations directory, you should see our newly generate migration file. We’re going to make a quick edit to this file. Since I haven’t been able to find a simple way to create seeder files with TypeORM, I generally modifiy the migrations file to add temporary seed data when running the migration. If there is a better way to do this, please reach out and let me know, I’d love to learn a better way. We will add a private method called seed() to our migration and call it after our table is created in the up(...) method of the migration class.

import { MigrationInterface, QueryRunner, getRepository } from 'typeorm';
import { User } from '../entities/user.entity';

export class GenerateAndSeedUserTable1585862017523 implements MigrationInterface {
  name = 'generateAndSeedUserTable1585862017523';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(
      `CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "username" varchar NOT NULL, "firstname" varchar NOT NULL, "lastname" varchar NOT NULL, CONSTRAINT "UQ_78a916df40e02a9deb1c4b75edb" UNIQUE ("username"))`,
      undefined,
    );
    await this.seed();
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "user"`, undefined);
  }

  private async seed(): Promise<void> {
    await getRepository(User).save([
      {
        id: 1,
        username: 'jmw5598',
        firstname: 'Jason',
        lastname: 'White',
      },
      {
        id: 2,
        username: 'djt2020',
        firstname: 'Daniel',
        lastname: 'Townswell',
      },
      {
        id: 3,
        username: 'dlw3512',
        firstname: 'Danielle',
        lastname: 'Whitemore',
      },
    ] as User[]);
  }
}

Now we can run our migrations with npm run migrations:run. Your output should look similar to this…

TypeORM migration run output

 

Connecting & Getting Our Users From the Database

Next we are going to refactor our UsersController by abstracting out our data access logic to our repository layer. We are going to follow some of SOLID priciples by hiding our repository implementation behind an interface. We will then use dependency inversion to provide a repository to our UsersController through the constructor.

Lets start by creating files for our repository interface and our users repository implementation.

touch src/app/repositories/repository.interface.ts src/app/repositories/users.repository.ts

Our simple repository interface…

export interface IRepository<T> {
  findAll(): Promise<T[]>;
}

Our users repository implementation which will implement our IRepository

import { getRepository, Repository } from 'typeorm';
import { IRepository } from './repository.interface';
import { User } from '../data';

export class UsersRepository implements IRepository<User> {
  public async findAll(): Promise<User[]> {
    const repository: Repository<User> = getRepository(User);
    return repository.find();
  }
}

Update our repositories/ directory’s barrel file (src/app/repositories/index.ts)…

export * from './repository.interface';
export * from './users.repository';

Now we can update our controller to use dependency inversion by accepting an IRepository as a constructor argument. We will also update our getAllUsers(...) method to use that repository.

import { Request, Response, NextFunction } from 'express';
import { IRepository } from '../repositories';
import { User } from '../data';

export class UsersController {
  private readonly _repository: IRepository<User>;

  constructor(repository: IRepository<User>) {
    this._repository = repository;
  }

  public async getAllUsers(request: Request, response: Response, next: NextFunction): Promise<any> {
    return this._repository
      .findAll()
      .then((users) => response.status(200).send(users))
      .catch((error) => response.status(500).send({ error: error }));
  }
}

Update our users.router.ts to create and provide an IRepository to our UsersController

import express, { Request, Response, Router, NextFunction } from 'express';
import { UsersController } from '../controllers';
import { IRepository, UsersRepository } from '../repositories';
import { User } from '../data';

const router: Router = express.Router();
const usersRepository: IRepository<User> = new UsersRepository();
const controller: UsersController = new UsersController(usersRepository);

router.get('/', async (request: Request, response: Response, next: NextFunction) => {
  await controller.getAllUsers(request, response, next);
});

export const usersRouter: Router = router;

By using depenency inversion, our controller is loosely coupled with our data access layer and not tighly tied to TypeORM, so down the road if we wanted to switch to sequelize or maybe just raw SQL queries and ditch the ORM completely, we won’t have to change any logic in our Controller (or anywhere that uses an IRepository interface). We would simply create new implentation of our IRepository and provide the new implementation to the constructor of our controller. Our code becomes more play-doh like and less like trying to bend glass.

Using dependency inversion also makes testing easier for us as well. Since we’re able to control the dependencies of our UsersController, we’re able to create a mock repository when isolating and testing our UsersControllers methods. You can learn a bit more about SOLID priciples here

 

The completed github repository can be found here

 


comments powered by Disqus