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 9 commits into
base: 02-express-middlewares-and-request-handlers
from
@@ -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

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -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",

This file was deleted.

Oops, something went wrong.
@@ -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)

@@ -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.
@@ -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)
@@ -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)
}
}
@@ -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
}
@@ -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)
}
}
}
@@ -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
}, {})
@@ -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.
@@ -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
}
}
File renamed without changes.
@@ -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()
}
@@ -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
}
}
}
@@ -0,0 +1,4 @@
declare module 'connect-datadog' {
const x: any
export = x
}
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.