Back to Node

Featured image

To prove to myself that I can still write JavaScript TypeScript, I’ve decided to write a series of posts on developing a small URL shortening app in Node.js. This first post will be about setting up the project. The code is available on GitHub.

Update: Parts 2, 3 and 4 are now available.

Javascript Dev

It’s been a while since I used Node.js in anger. It used to be my go to technology for writing web backends; allowing me to use the same language right through to the UI. In the past I’ve used Node to build:

  • a streaming notification service using websockets and serving hundreds of thousands of messages a day
  • a multi-terabyte text search and analysis platform using Elasticsearch
  • the backend for a mobile, realtime, location-based, messaging application1 with a front end running on iOS and Android using React Native2

But it’s been a while since I’ve used Node and the tools supporting it have changed since then. I wanted to brush up on a number of technologies including:

Note: I decided to document my journey with this project after already playing around for a couple of hours. Some of the commands below come from my memory and zsh history file. They should be right…

The Project

To get back in to the swing of things I’ve decided to write a URL shortener. It will consist of a database, likely Postgres, to store the URLs and an API written in Node.js. I was going to use MongoDB initially for the data layer but TypeORM only has experimental support at this stage so Postgres it is.

The basic functionality of the API will be:

  • GET /url - get the existing shortened URLs probably with a search syntax or GraphQL functions
  • POST /url - add a new URL
  • GET /url/{urlId} - get basic stats around the shortened URL
  • DELETE /url/urlId - delete a shortened URL

I’ll use Swagger to document the API.

Typescript

In my last Node.js project I had started to use Flow for type checking in Javascript which made things much easier but was a pain to set up and often gave weird warnings I was never quite sure of. Typescript was a thing but it still seemed new and scary. Time to give it a proper try.

Adding Typescript to a project and configuring it is relatively easy.

npm add --save-dev typescript
# Configure Typescript
npx tsc --init --rootDir src --outDir dist
# Node and Express server specific type definitions for Typescript
npm add --save-dev @types/node @types/express
# Autoreloading with support for Typescript 
npm add --save-dev nodemon ts-node

In the end my tsconfig.json file looks like this.

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */
    "target": "es6",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "noImplicitAny": true,
    "noImplicitThis": true,
    "moduleResolution": "node",
    "baseUrl": "src",
    "paths": {
      "*": ["node_modules/*", "src/types/*"]
    },
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
}

And I added entries for build and dev in the package.json to compile the Typescript files.

  "scripts": {
    "build": "tsc",
    "dev": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts",
    ...
  }

Jest

Jest is a delightful JavaScript Testing Framework with a focus on simplicity. - Jest Homepage

npm install --save-dev jest
# Typescript support for Jest
npm add --save-dev @types/jest ts-jest

I’d used Jest in my previous project and it was easy to set up and start testing. I just had to add the Typescript specific settings into the jest.config.js file as shown below.

module.exports = {
  'roots': [
    './src'
  ],
  'testMatch': [
    '**/__tests__/**/*.+(ts|tsx|js)',
    '**/?(*.)+(spec|test).+(ts|tsx|js)'
  ],
  'transform': {
    '^.+\\.(ts|tsx)$': 'ts-jest'
  },
  'coverageDirectory': './coverage',
  'collectCoverageFrom': [
    'src/**/*.ts',
    '!**/node_modules/**',
    '!**/vendor/**'
  ]
}

And update the test script in package.json. I added the option to generate code coverage artifacts into the ./coverage directory.

  "scripts": {
    "test": "jest --coverage",
    "build": "tsc",
    ...
  }

Eslint

I’m not going to go into too much detail here because this was a scarring experience. Getting Eslint to work with Typescript was more trouble than I imagined it would be.

It wasn’t helped by the fact I decided to go ahead and use the yaml output for the eslintrc file rather than json. I would not recommend that. Almost none of the examples or sample code from anywhere is in yaml.

The commands below are what I used but I don’t know exactly how many packages are actually required. I need to remove them one-by-one and blow away my node-modules directory to see which onces I do actually need.

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin @typescript-eslint/eslint-recommended

The rules.import/extensions and settings.import/resolver sections were just copied and pasted from several Stack Overflow suggestions on how to fix Missing file extension "ts" for "./module" import/extensions errors. I don’t know exactly what these do and at this point I’m too afraid to ask.

env:
  browser: true
  es2020: true
  jest: true
extends:
  - airbnb-base  # I'd used this in the past so went with it again
  - plugin:import/typescript
  - plugin:@typescript-eslint/eslint-recommended
  - plugin:@typescript-eslint/recommended
parser: '@typescript-eslint/parser'
parserOptions:
  ecmaVersion: 11
  sourceType: module
plugins:
  - '@typescript-eslint'
ignorePatterns:
  - jest.config.js
rules:
  import/extensions:
    - error
    - ignorePackages
    - {
      js: never,
      mjs: never,
      jsx: never,
      ts: never,
      tsx: never,
      vue: never,
    }
settings:
  import/resolver:
    node:
      extensions:
        - .js
        - .jsx
        - .ts
        - .tsx

Swagger

I’ve used Swagger extensively for documenting APIs and setting it up with Node so the definition is served was pretty easy. The only thing I needed to do was install yamljs to convert the YAML file into JSON which swagger-ui-express expected.

# Add support for serving Swagger definition file
npm add swagger-ui-express 
npm add yamljs  # Convert Swagger yaml file to json

# Typescript definitions
npm add --save-dev @types/swagger-ui-express @types/yamljs  

TypeORM

TypeORM is an ORM that can run in NodeJS, Browser, Cordova, PhoneGap, Ionic, React Native, NativeScript, Expo, and Electron platforms and can be used with TypeScript and JavaScript (ES5, ES6, ES7, ES8). - TypeORM Homepage

# TypeORM with Postgres support
npm install typeorm pg --save
# Also had to install this for some reason...
npm install reflect-metadata --save

As instructed I added the following options to my tsconfig.json file.

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,    /* Required by TypeORM */
    "experimentalDecorators": true,   /* Required by TypeORM */
    ...
  }
}

Circular Dependencies in TypeORM

When I started playing with TypeORM I started to map out One-to-Many and Many-to-One dependencies. This resulted in some weird circular dependencies that has been discussed on GitHub.

// ShortenedUrl.ts
import URLLogEntry from './URLLogEntry'

@Entity()
export default class ShortenedUrl {
  // ...
  @OneToMany(type => URLLogEntry**, (entry) => entry.logEntries)
  logEntries!: URLLogEntry;
  //...
}
// URLLogEntry.ts
import ShortenedUrl from './ShortenedUrl'

@Entity()
export default class URLLogEntry {
  // ...
  @ManyToOne(type => ShortenedUrl, (url) => url.logEntries)
  urlId!: ShortenedUrl;
  //...
}

Like others in that thread mentioned, I wasn’t sure how you could use this library given the circular dependencies. Turns out you can use strings instead of concrete classes to set up the mappings and use interfaces instead of classes which avoids having circular dependencies.

// URLLogEntry.ts
@ManyToOne('ShortenedUrl', 'logEntries')
url!: IShortenedUrl;

Setup Done 👍

Simple. I forgot how many things there are to set up when working with a JavaScript/TypeScript project. But it feels like it’s all come together now. Now I get to start writing code. Or at least writing the tests for that code.

Stay tuned for part 2.


  1. That should generate some sweet SEO ↩︎

  2. Unfortunately this app never made it into production because I wanted to give it away for free instead of charging hundreds of thousands of dollars per year ↩︎