Powering your Back-End applications with TypeScript
Darío Blanco Iturriaga
CTO @ MindDoc
April 11, 2019
The "Small Team" Problem

Web Application
- Client-side?
- Server-side?
Front-End and Back-End tech stack should be as small as possible
Front-End frameworks
- SPA "monopoly"
- Embrace JavaScript

But... JavaScript sucks
var Rectangle = function (id, x, y, width, height) {
Shape.call(this, id, x, y);
this.width = width;
this.height = height;
};
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var Circle = function (id, x, y, radius) {
Shape.call(this, id, x, y);
this.radius = radius;
};
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;ECMAScript made it better
class Rectangle extends Shape {
constructor (id, x, y, width, height) {
super(id, x, y)
this.width = width
this.height = height
}
}
class Circle extends Shape {
constructor (id, x, y, radius) {
super(id, x, y)
this.radius = radius
}
}Back-End should ideally have...
- Same language (e.g. JavaScript)
- Similar tooling (e.g. test framework)
- Statically-typed
Oh wait... JavaScript is not statically-typed!

Let's use TypeScript everywhere
The Tech Stack




Same test framework is a great productivity win
Bootstraping Node

Basic Skeleton
Project manifest and dependencies
TypeScript root files and compiler options
TypeScript lint rules
TypeScript source files
TypeScript Jest test specs

Jest test configuration
package.json
{
"name": "typescript-node-starter",
"version": "1.0.0",
"description": "A TypeScript Node skeleton project",
"main": "src/server.ts",
"scripts": {
"compile": "tsc",
"lint": "tslint --project .",
"test": "jest --coverage"
},
"repository": {
"type": "git",
"url": "git+https://github.com/minddocdev/typescript-node-starter.git"
},
"keywords": [
"typescript",
"node",
"skeleton",
"starter"
],
"author": "development@minddoc.com",
"license": "ISC",
"bugs": {
"url": "https://github.com/minddocdev/typescript-node-starter/issues"
},
"homepage": "https://github.com/minddocdev/typescript-node-starter#readme",
"devDependencies": {
"@types/express": "^4.16.1",
"@types/jest": "^24.0.11",
"@types/node": "^11.13.4",
"jest": "^24.7.1",
"ts-jest": "^24.0.2",
"tslint": "^5.15.0",
"tslint-config-airbnb": "^5.11.1"
},
"dependencies": {
"express": "^4.16.4",
"typescript": "^3.4.3"
}
}
{
"compilerOptions": {
"baseUrl": ".",
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"dom",
"es2017"
],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": [
"src/*"
],
"#/*": [
"test/*"
]
}
},
"exclude": [
"node_modules",
],
"include": [
"src/**/*.ts",
"test/**/*.ts",
]
}
tsconfig.json

tsc
{
"defaultSeverity": "error",
"extends": [
"tslint-config-airbnb"
],
"jsRules": {},
"rules": {
"import-name": false,
"no-duplicate-variable": true
},
"rulesDirectory": []
}
tslint.json

tslint --project .
const path = require('path');
module.exports = {
collectCoverageFrom: [
'src/**/*.ts',
'!src/server.ts',
],
coverageDirectory: '<rootDir>/test/coverage',
coverageReporters: process.env.CI ? ['text'] : ['json', 'lcov', 'text', 'clover'],
coverageThreshold: {
global: {
branches: 100,
functions: 100,
lines: 100,
},
},
globals: {
'ts-jest': {
tsConfig: 'tsconfig.json',
}
},
moduleFileExtensions: [
'json',
'ts',
'js',
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'^#/(.*)$': '<rootDir>/test/$1',
},
rootDir: path.resolve(__dirname),
testEnvironment: 'node',
testMatch: [
'**/test/**/*.spec.(ts|js)',
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
};
jest.config.js
jest --coverage

Running Node

- Alternative to node (ts-node)
- Executes TypeScript files directly
- Source map support
- Loads tsconfig.json automatically
- Development version for file changes (ts-node-dev)
- Module loading (tsconfig-paths)

tsconfig-paths
{
"compilerOptions": {
"baseUrl": ".",
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"dom",
"es2017"
],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": [
"src/*"
],
"#/*": [
"test/*"
]
}
},
"files": [
"node_modules/@types/node/index.d.ts",
"node_modules/@types/jest/index.d.ts",
"node_modules/@types/express/index.d.ts"
],
"exclude": [
"node_modules",
],
"include": [
"src/**/*.ts",
"test/**/*.ts",
]
}
- Relative imports can be terrible (e.g. ../../../../myfolder/foo)
- Arbitrary module paths are nice (e.g. @/myfolder/foo)
- Use tsconfig-paths/register in the ts-node command to allow them
{
"name": "typescript-node-starter",
"version": "1.0.0",
"description": "A TypeScript Node skeleton project",
"main": "src/server.ts",
"scripts": {
"compile": "tsc",
"dev": "ts-node-dev -r tsconfig-paths/register --ignore-watch node_modules src/server.ts",
"lint": "tslint --project .",
"start": "ts-node --transpile-only -r tsconfig-paths/register src/server.ts",
"test": "jest --coverage"
},
"repository": {
"type": "git",
"url": "git+https://github.com/minddocdev/typescript-node-starter.git"
},
"keywords": [
"typescript",
"node",
"skeleton",
"starter"
],
"author": "development@minddoc.com",
"license": "ISC",
"bugs": {
"url": "https://github.com/minddocdev/typescript-node-starter/issues"
},
"homepage": "https://github.com/minddocdev/typescript-node-starter#readme",
"devDependencies": {
"@types/express": "^4.16.1",
"@types/jest": "^24.0.11",
"@types/node": "^11.13.4",
"jest": "^24.7.1",
"ts-jest": "^24.0.2",
"ts-node-dev": "^1.0.0-pre.32",
"tslint": "^5.15.0",
"tslint-config-airbnb": "^5.11.1"
},
"dependencies": {
"express": "^4.16.4",
"ts-node": "^8.0.3",
"tsconfig-paths": "^3.8.0",
"typescript": "^3.4.3"
}
}
package.json

ts-node-dev -r tsconfig-paths/register --ignore-watch node_modules src/server.ts
- Reloads server on file changes (except node_modules)
- Registers tsconfig compiler paths
# ------------------------------------------------------
# Dockerfile
# ------------------------------------------------------
# image: typescript-node-starter
# tag: latest
# name: minddocdev/typescript-node-starter
# repo:
# how-to: docker build -t minddocdev/typescript-node-starter:latest .
# Requires: node:alpine
# authors: development@minddoc.com
# ------------------------------------------------------
FROM node:11.13-alpine
LABEL maintainer="development@minddoc.com"
# Create app directory and install production dependencies
WORKDIR /usr/src/app
COPY tsconfig.json package.json package-lock.json ./
RUN npm install --only=production
# Copy src files (Use .dockerignore to exclude non essential)
COPY src/ ./src
# Set permissions for the node user
RUN chown -R node:node .
USER node
# Run ts-node with src/server.ts entry point
CMD ["npm", "start"]
Dockerfile
{
"name": "typescript-node-starter",
"version": "1.0.0",
"description": "A TypeScript Node skeleton project",
"main": "src/server.ts",
"scripts": {
"compile": "tsc",
"dev": "ts-node-dev -r tsconfig-paths/register --ignore-watch node_modules src/server.ts",
"docker": "docker build -t minddocdev/typescript-node-starter:latest . && docker run -it minddocdev/typescript-node-starter:latest",
"lint": "tslint --project .",
"start": "ts-node --transpile-only -r tsconfig-paths/register src/server.ts",
"test": "jest --coverage"
},
"repository": {
"type": "git",
"url": "git+https://github.com/minddocdev/typescript-node-starter.git"
},
"keywords": [
"typescript",
"node",
"skeleton",
"starter"
],
"author": "development@minddoc.com",
"license": "ISC",
"bugs": {
"url": "https://github.com/minddocdev/typescript-node-starter/issues"
},
"homepage": "https://github.com/minddocdev/typescript-node-starter#readme",
"devDependencies": {
"@types/express": "^4.16.1",
"@types/jest": "^24.0.11",
"@types/node": "^11.13.4",
"jest": "^24.7.1",
"ts-jest": "^24.0.2",
"ts-node-dev": "^1.0.0-pre.32",
"tslint": "^5.15.0",
"tslint-config-airbnb": "^5.11.1"
},
"dependencies": {
"express": "^4.16.4",
"ts-node": "^8.0.3",
"tsconfig-paths": "^3.8.0",
"typescript": "^3.4.3"
}
}
package.json

Production app!
Create your types

Working with types
- TypeScript automatically includes node_modules/@types packages by default
- node_modules/@types are devDependencies.
- Automatic inclusion is only important for global declarations
- You can always use import { ExternalType } from 'external-lib';
{
"compilerOptions": {
"baseUrl": ".",
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"sourceMap": true,
"strict": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"dom",
"es2017"
],
"skipLibCheck": true,
"noEmit": true,
"paths": {
"@/*": [
"src/*"
],
"#/*": [
"test/*"
]
},
"typeRoots": [
"types"
],
"types": [
"dependencies",
"mytypes"
]
},
"files": [
"node_modules/@types/node/index.d.ts",
"node_modules/@types/jest/index.d.ts",
"node_modules/@types/express/index.d.ts"
],
"exclude": [
"node_modules",
],
"include": [
"src/**/*.ts",
"test/**/*.ts",
"types/**/*.d.ts"
]
}
tsconfig.json
Only packages under ./types are global
Only specific packages from ./types will be global (optional)
Set specific node_modules types as global
import * as express from 'express';
export const expressApp = express();
/* istanbul ignore next */
expressApp.get('/', (req, res) => {
const response: HelloWorldResponse = { message: 'Hello World!' };
res.send(response);
});
/**
* Start the express application.
*
* @param port The port to listen to.
*/
export function start(port = 3000) {
expressApp.listen(port, (err: any) => {
if (err) {
console.error(`Unable to start app. Found error: ${err.message}`);
return;
}
console.info(`Example app listening on port ${port}!`);
});
}
// Type package for own global type definitions
interface HelloWorldResponse {
message: string;
}
src/app.ts
types/mytypes/index.d.ts
No need to import HelloWorldResponse


tsc will complain
What next?

As we grow, there are new challenges
- Shared types
- Configuration duplication
- Common dependencies
Scaling into Back-End microservices makes this worse
Shared types
- Front-End and Back-End share same types (e.g. API requests and responses)
- Initial solution: internal @types package used as dependency
More repository overhead
Configuration duplication
- tsconfig.json, tslint.json, jest.config.js, .gitlab-ci.yml are the same or very similar in our TypeScript projects
- Initial solution: copied files, which is bad
Common dependencies

- Update bot creates a lot of duplicated MRs in different repos
- Internal packages require more repos, duplication and overhead
Monorepo
The solution

Monorepo
- Only for TypeScript projects
- Lerna handles multiple packages
- Lerna hoists common dependencies
- Gitlab CI triggers specific pipelines based on file changes
- Gitlab CI avoids config duplication via YAML Anchors


@minddoc
@darioblanco

@minddocdev
@darioblanco
https://stackoverflow.com/jobs/companies/minddoc-by-schoen-clinic
https://github.com/minddocdev/typescript-node-starter

Powering your Back-End applications with TypeScript
By Darío Blanco
Powering your Back-End applications with TypeScript
- 253