Skip to content
9 changes: 9 additions & 0 deletions docs/03-server-application-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## 03 – Cut and design of the Backend

This branch introduces a proposal how to cut features in the backend: where to put data access, business logics and request handling.

Like always, `npm start` the server and try out the new enpoints:

1. `http://localhost:8000/api/cat` – Get all available cats
2. `http://localhost:8000/api/cat/1` – Now there are some more cats and details available. Try Cat ID 1 to 5.
3. `http://localhost:800/api/statistics/cat` – It responds statistics about all cats
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,23 @@
"name": "node-express-typescript-boilerplate",
"version": "0.0.1",
"description": "Boilerplate for developing web apps with Node.js, Express.js, Webpack and TypeScript",
"main": "service/index.ts",
"main": "service/server/index.ts",
"engines": {
"node": ">=8.15.0",
"npm": ">=6.4.1"
},
"scripts": {
"start": "ts-node service/index.ts",
"start": "ts-node service/server/index.ts",
"prettier:check": "prettier --check service/**/*.{ts,tsx,js,jsx}",
"prettier:write": "prettier --write service/**/*.{ts,tsx,js,jsx}"
},
"dependencies": {
"express": "^4.16.2",
"compression": "1.7.2",
"cookie-parser": "1.4.3",
"http-status-codes": "1.3.0"
"http-status-codes": "1.3.0",
"connect-datadog": "^0.0.6",
"hot-shots": "^4.7.0"
},
"devDependencies": {
"@types/express": "4.11.1",
Expand Down
21 changes: 0 additions & 21 deletions service/cats/CatEndpoints.ts

This file was deleted.

7 changes: 6 additions & 1 deletion service/Application.ts → service/server/Application.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { ExpressServer } from './ExpressServer'
import { CatEndpoints } from './cats/CatEndpoints'
import { CatService } from './cats/CatService'
import { CatRepository } from './cats/CatRepository'

/**
* Wrapper around the Node process, ExpressServer abstraction and complex dependencies such as services that ExpressServer needs.
* When not using Dependency Injection, can be used as place for wiring together services which are dependencies of ExpressServer.
*/
export class Application {
public static async createApplication() {
const expressServer = new ExpressServer(new CatEndpoints())
const catService = new CatService(new CatRepository())
const requestServices = { catService }
const expressServer = new ExpressServer(new CatEndpoints(), requestServices)

await expressServer.setup(8000)
Application.handleExit(expressServer)

Expand Down
25 changes: 22 additions & 3 deletions service/ExpressServer.ts → service/server/ExpressServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import * as compress from 'compression'
import * as bodyParser from 'body-parser'
import * as cookieParser from 'cookie-parser'

import { noCache } from './NoCacheMiddleware'
import { noCache } from './middlewares/NoCacheMiddleware'
import DatadogStatsdMiddleware from './middlewares/DatadogStatsdMiddleware'
import { CatEndpoints } from './cats/CatEndpoints'
import { RequestServices } from './types/CustomRequest'
import { addServicesToRequest } from './middlewares/ServiceDependenciesMiddleware'

/**
* Abstraction around the raw Express.js server and Nodes' HTTP server.
Expand All @@ -17,12 +20,13 @@ export class ExpressServer {
private server?: Express
private httpServer?: Server

constructor(private catEndpoints: CatEndpoints) {
}
constructor(private catEndpoints: CatEndpoints, private requestServices: RequestServices) {}

public async setup(port: number) {
const server = express()
this.setupStandardMiddlewares(server)
this.setupTelemetry(server)
this.setupServiceDependencies(server)
this.configureApiEndpoints(server)

this.httpServer = this.listen(server, port)
Expand All @@ -44,7 +48,22 @@ export class ExpressServer {
server.use(compress())
}

private setupTelemetry(server: Express) {
DatadogStatsdMiddleware.applyTo(server, {
targetHost: 'https://datadog.mycompany.com',
enableTelemetry: false,
tags: ['team:cats', 'product:cats-provider']
})
}

private setupServiceDependencies(server: Express) {
const servicesMiddleware = addServicesToRequest(this.requestServices)
server.use(servicesMiddleware)
}

private configureApiEndpoints(server: Express) {
server.get('/api/cat', noCache, this.catEndpoints.getAllCats)
server.get('/api/statistics/cat', noCache, this.catEndpoints.getCatStatistics)
server.get('/api/cat/:catId', noCache, this.catEndpoints.getCatDetails)
}
}
14 changes: 14 additions & 0 deletions service/server/cats/Cat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type CatGender = 'female' | 'male' | 'diverse'

export interface Cat {
id: number
name: string
breed: string
gender: CatGender
age: number
}

export interface CatsStatistics {
amount: number
averageAge: number
}
37 changes: 37 additions & 0 deletions service/server/cats/CatEndpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { NextFunction, Request, Response } from 'express'
import * as HttpStatus from 'http-status-codes'

export class CatEndpoints {
public getCatDetails = async (req: Request, res: Response, next: NextFunction) => {
try {
const catId = req.params.catId
const cat = req.services.catService.getCat(catId)

if (cat) {
res.json(cat)
} else {
res.sendStatus(HttpStatus.NOT_FOUND)
}
} catch (err) {
// something could fail unexpectedly...
// at some point the middleware chain should handle errors
next(err)
}
}

public getAllCats = async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(req.services.catService.getAllCats())
} catch (err) {
next(err)
}
}

public getCatStatistics = async (req: Request, res: Response, next: NextFunction) => {
try {
res.json(req.services.catService.getCatsStatistics())
} catch (err) {
next(err)
}
}
}
54 changes: 54 additions & 0 deletions service/server/cats/CatRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Cat } from './Cat'

export class CatRepository {
public getById(id: number): Cat | undefined {
return catsById[id]
}

public getAll(): Cat[] {
return cats
}
}

const cats: Cat[] = [
{
id: 1,
name: 'Tony Iommi',
breed: 'British Shorthair',
gender: 'male',
age: 71
},
{
id: 2,
name: 'Ozzy Osbourne',
breed: 'British Semi-longhair',
gender: 'male',
age: 70
},
{
id: 3,
name: 'Geezer Butler',
breed: 'British Longhair',
gender: 'male',
age: 69
},
{
id: 4,
name: 'Bill Ward',
breed: 'Burmilla',
gender: 'male',
age: 70
},
{
id: 5,
name: 'Sharon Osbourne',
breed: 'Bambino',
gender: 'female',
age: 66
}
]
type CatsById = { [id: number]: Cat }
const catsById: CatsById = cats.reduce((catzById: CatsById, currentCat) => {
catzById[currentCat.id] = currentCat
return catzById
}, {})
25 changes: 25 additions & 0 deletions service/server/cats/CatService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { CatRepository } from './CatRepository'
import { CatsStatistics, Cat } from './Cat'

export class CatService {
constructor(private catRepository: CatRepository) {
}

public getCat(id: number): Cat | undefined {
return this.catRepository.getById(id)
}

public getAllCats(): Cat[] {
return this.catRepository.getAll()
}

public getCatsStatistics(): CatsStatistics {
const allCats = this.catRepository.getAll()
const catsAgeSum = allCats.map(cat => cat.age).reduce((sum: number, nextAge: number) => sum + nextAge, 0)

return {
amount: allCats.length,
averageAge: catsAgeSum / allCats.length
}
}
}
File renamed without changes.
36 changes: 36 additions & 0 deletions service/server/middlewares/DatadogStatsdMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Express } from 'express'
import * as StatsD from 'hot-shots'
import * as connectDatadog from 'connect-datadog'

export interface DatadogStatsdConfig {
targetHost: string
enableTelemetry: boolean
tags: string[]
}

export default class DatadogStatsdMiddleware {
public static applyTo(server: Express, config: DatadogStatsdConfig) {
const statsdClient = DatadogStatsdMiddleware.createStatsdClient({
host: config.targetHost,
mock: !config.enableTelemetry
})

const datadogStatsdMiddleware = connectDatadog({
dogstatsd: statsdClient,
tags: config.tags,
path: false,
method: true,
response_code: false
})

server.use(datadogStatsdMiddleware)
}

private static createStatsdClient(options?: StatsD.ClientOptions) {
const statsdClient = new StatsD(options)
statsdClient.socket.on('error', (err: any) => {
console.error('Error sending datadog stats', err)
})
return statsdClient
}
}
7 changes: 7 additions & 0 deletions service/server/middlewares/ServiceDependenciesMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Request, Response, NextFunction, RequestHandler } from 'express'
import { RequestServices } from '../types/CustomRequest'

export const addServicesToRequest = (services: RequestServices): RequestHandler => (req: Request, res: Response, next: NextFunction) => {
(req as any).services = services
next()
}
15 changes: 15 additions & 0 deletions service/server/types/CustomRequest.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/* tslint:disable no-namespace */
import 'express'
import { CatService } from '../cats/CatService'

export interface RequestServices {
catService: CatService
}

declare global {
namespace Express {
interface Request {
services: RequestServices
}
}
}
4 changes: 4 additions & 0 deletions service/server/types/connect-datadog/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module 'connect-datadog' {
const x: any
export = x
}
Loading