Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes 02-03 – Server application design #3

Open
wants to merge 10 commits into
base: 02-express-middlewares-and-request-handlers
Choose a base branch
from
9 changes: 9 additions & 0 deletions docs/03-server-application-design.md
@@ -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
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
@@ -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
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,4 @@
declare module 'connect-datadog' {
const x: any
export = x
}