<file_sep>import { ActionModel } from '../actions/actions.model'

export interface DogModel {
	id: number
	name: string
	breed: string
	age: number
	actionIds: number[]
	actions?: ActionModel[]
}
<file_sep>import morgan from 'morgan'
import { Request, Response, NextFunction } from 'express'
import { LoggerStream } from 'Src/pkg/log/log'

let format: string | null = null

export function morganLoggerMiddleware(
	req: Request,
	res: Response,
	next: NextFunction,
): Function {
	if (!format) {
		format = process.env.NODE_ENV === 'production' ? 'combined' : 'tiny'
	}

	return morgan(format, { stream: new LoggerStream() })(req, res, next)
}
<file_sep>FROM node:10.16.0-alpine
WORKDIR /app
COPY . ./
RUN yarn
EXPOSE 8000<file_sep>interface ActionEntity {
	id: number
	name: string
}

const actions: ActionEntity[] = [
	{ id: 1, name: 'sit' },
	{ id: 2, name: 'bark' },
	{ id: 3, name: 'salute' },
	{ id: 4, name: 'stand' },
	{ id: 5, name: 'jump' },
	{ id: 6, name: 'roll' },
	{ id: 7, name: 'crawl' },
]

type FindAllResponse = Array<ActionEntity>
type FindByIdResponse = ActionEntity

export class ActionsRepository {
	findAll(): FindAllResponse {
		return actions
	}

	findById(id: number): FindByIdResponse | null {
		const action = actions.find((a) => a.id === id)
		if (!action) {
			return null
		}
		return action
	}
}
<file_sep>import { DogsService } from './dog.service'
import { DogsRepository } from '../storage/memory/dog.repository'
import { ActionsRepository } from '../storage/memory/actions.repository'
import { DogModel } from './dog.model'

describe('DogsService', () => {
	let dogsService: DogsService

	beforeEach(() => {
		const actionsRepository = new ActionsRepository()
		const dogsRepository = new DogsRepository()
		dogsService = new DogsService(actionsRepository, dogsRepository)
	})

	it('should be defined', () => {
		expect(dogsService).toBeDefined()
	})

	describe('getAll', () => {
		it('On success: should return a list of dogs', () => {
			const response = dogsService.getAll()

			const objectMatcher: DogModel = {
				id: expect.any(Number),
				name: expect.any(String),
				breed: expect.any(String),
				age: expect.any(Number),
				actionIds: expect.arrayContaining([expect.any(Number)]),
			}

			expect(response).toBeTruthy()
			expect(response).toHaveProperty('length')
			expect(response.length).toBeGreaterThan(0)
			expect(response[0]).toMatchObject(objectMatcher)
		})
	})
})
<file_sep>module.exports = {
	arrowParens: 'always',
	bracketSpacing: true,
	endOfLine: 'lf',
	inserPragma: false,
	printWidth: 80,
	quoteProps: 'consistent',
	requirePragma: false,
	semi: false,
	singleQuote: true,
	tabWidth: 2,
	trailingComma: 'all',
	useTabs: true,
}
<file_sep>import { Express } from 'express'
import request from 'supertest'
import { bootstrapRestServer } from '../../src/cmd/server/rest'

describe.only('REST: Actions', () => {
	let app: Express

	beforeAll(async () => {
		app = await bootstrapRestServer()
	})

	describe('GET: /api/actions', () => {
		it('on success: should return a list of actions', async () => {
			const response = await request(app).get('/api/actions')
			console.log(response.body)
			const itemsMatcher = {
				items: expect.arrayContaining([
					expect.objectContaining({
						id: expect.any(Number),
						name: expect.any(String),
					}),
				]),
			}
			expect(response.status).toBe(200)
			expect(response.body.count).toBeGreaterThanOrEqual(0)
			expect(response.body.items).toMatchObject(itemsMatcher.items)
		})
	})
})
<file_sep>import { Express } from 'express'
import { DogsRepository } from 'Src/pkg/storage/memory/dog.repository'
import { DogsService } from 'Src/pkg/dogs/dog.service'
import { createRestServer } from 'Src/pkg/http/rest'
import { ActionsRepository } from 'Src/pkg/storage/memory/actions.repository'
import { ActionsService } from 'Src/pkg/actions/actions.service'

export async function bootstrapRestServer(): Promise<Express> {
	const actionsRepository = new ActionsRepository()
	const dogsRepository = new DogsRepository()
	const actionsService = new ActionsService(actionsRepository)
	const dogService = new DogsService(actionsRepository, dogsRepository)

	const server = createRestServer(actionsService, dogService)
	return server
}

bootstrapRestServer()
	.then((server) =>
		server.listen(3000, () => {
			console.log('server listening on port: 3000')
		}),
	)
	.catch((error) => console.log(error))
<file_sep>import { Request, Response, NextFunction } from 'express'
import { logger } from 'Src/pkg/log/log'

export function routeNotFoundMiddleware(
	req: Request,
	res: Response,
	next: NextFunction, // eslint-disable-line
): Response {
	logger.info(`Route: ${req.originalUrl}, not found`)
	return res.status(404).send("Sorry can't find that!") // eslint-disable-line
}
<file_sep>interface DogEntity {
	id: number
	name: string
	breed: string
	age: number
	actionIds: number[]
}

const dogs: DogEntity[] = [
	{
		id: 1,
		name: 'Stormy',
		breed: '<NAME>',
		age: 6,
		actionIds: [2, 3],
	},
	{
		id: 2,
		name: 'Stormy',
		breed: '<NAME>',
		age: 9,
		actionIds: [1, 2, 3, 4, 5, 6],
	},
]

type FindAllResponse = Array<DogEntity>
type FindByIdResponse = DogEntity

export class DogsRepository {
	public findAll(): FindAllResponse {
		return dogs
	}

	public findById(id: number): FindByIdResponse | null {
		const dog = dogs.find((d) => d.id === id)
		if (!dog) {
			return null
		}
		return dog
	}
}
<file_sep>import { ErrorRequestHandler, Request, Response, NextFunction } from 'express'
import { logger } from 'Src/pkg/log/log'

export function errorHandlerMiddleware(
	err: ErrorRequestHandler,
	req: Request,
	res: Response,
	next: NextFunction, // eslint-disable-line
): Response {
	logger.error(err)
	return res.status(500).send(err)
}
<file_sep>FROM node:10.12.0-alpine as base
WORKDIR /app
COPY package.json yarn.lock babel.config.js tsconfig.json ./
RUN yarn
COPY src /app/src
RUN yarn build

FROM node:10.12.0-alpine
COPY package.json yarn.lock ./
RUN yarn --production
COPY --from=base /app/dist /app/dist
EXPOSE 8000
<file_sep>import { Router, Request, Response } from 'express'

import { DogsService } from 'Src/pkg/dogs/dog.service'

function getAllDogsHandler(dogsService: DogsService) {
	return (req: Request, res: Response) => {
		const dogs = dogsService.getAll()

		return res.json({
			count: dogs.length,
			items: dogs,
		})
	}
}

function getActionsByDog(dogsService: DogsService) {
	return (req: Request, res: Response) => {
		const params = req.params as { dogId: string }
		if (!params.dogId) {
			return res.status(400).json({
				error: 'BAD REQUEST',
				message: 'Missing dog ID in params',
			})
		}
		const actions = dogsService.getActionsByDog(parseInt(params.dogId, 10))
		if (!actions) {
			return res.status(404).json({
				error: 'NOT FOUND',
				message: 'The dog with the specified id does not exist',
			})
		}
		return res.json({
			data: actions,
		})
	}
}

export function createDogsHandler(
	router: Router,
	dogsService: DogsService,
): void {
	router.get('/api/dogs', getAllDogsHandler(dogsService))
	router.get('/api/dogs/:dogId/actions', getActionsByDog(dogsService))
}
<file_sep>import { Router, Request, Response } from 'express'

import { ActionsService } from 'Src/pkg/actions/actions.service'

function getAllActionsHandler(actionsService: ActionsService) {
	return (req: Request, res: Response) => {
		const actions = actionsService.getAll()

		return res.json({
			count: actions.length,
			items: actions,
		})
	}
}

export function createActionsHandler(
	router: Router,
	actionsService: ActionsService,
): void {
	router.get('/api/actions', getAllActionsHandler(actionsService))
}
<file_sep># Express Boilerplate App

## A simple express app for quickly getting started with new projects

-   Simple folder/project structure
-   Dockerfiles and docker-compose for quick starting the app
-   Editor and prettier config files for code styling
-   Vscode config file
-   Typescript 3.5
-   Babel 7
-   Testing with Jest
-   Logging with Winston

## Useful commands

1. yarn dev: build in development and watch for changes with nodemon

2. yarn build: build the app and output it to the dist folder

3. yarn test: run jest

4. yarn type-check: use typescript type checks
<file_sep>import { ActionsRepository } from 'Src/pkg/storage/memory/actions.repository'
import { DogsRepository } from 'Src/pkg/storage/memory/dog.repository'
import { ActionModel } from '../actions/actions.model'
import { DogModel } from './dog.model'

export class DogsService {
	constructor(
		private readonly actionsRepository: ActionsRepository,
		private readonly dogsRepository: DogsRepository,
	) {}

	private makeDog(data: any): DogModel {
		return {
			id: data.id,
			name: data.name,
			breed: data.breed,
			age: data.age,
			actionIds: data.actionIds,
		}
	}

	public getAll(): DogModel[] {
		const dogs = this.dogsRepository.findAll()
		return dogs.map((d) => this.makeDog(d))
	}

	public getActionsByDog(id: number): ActionModel[] | null {
		const dog = this.dogsRepository.findById(id)
		if (!dog) {
			return null
		}
		const actions = this.actionsRepository.findAll()
		const response = actions.filter((a) => dog.actionIds.includes(a.id))
		return response
	}
}
<file_sep>import { Request, Response, NextFunction } from 'express'

export function noSniffMiddleware(
	req: Request,
	res: Response,
	next: NextFunction,
): void {
	res.setHeader('X-Content-Type-Options', 'nosniff')
	next()
}
<file_sep>import { ActionsService } from './actions.service'
import { ActionsRepository } from '../storage/memory/actions.repository'
import { ActionModel } from './actions.model'

describe('ActionsService', () => {
	let actionsService: ActionsService

	beforeEach(() => {
		const actionsRepository = new ActionsRepository()
		actionsService = new ActionsService(actionsRepository)
	})

	it('Should be defined', () => {
		expect(actionsService).toBeDefined()
	})

	describe('getAll', () => {
		it('On success: should return a list of actions', () => {
			const response = actionsService.getAll()

			const objectMatcher: ActionModel = {
				id: expect.any(Number),
				name: expect.any(String),
			}

			expect(response).toBeTruthy()
			expect(response).toHaveProperty('length')
			expect(response.length).toBeGreaterThan(0)
			expect(response[0]).toMatchObject(objectMatcher)
		})
	})
})
<file_sep>import { ActionsRepository } from 'Src/pkg/storage/memory/actions.repository'
import { ActionModel } from './actions.model'

export class ActionsService {
	constructor(private readonly actionsRepository: ActionsRepository) {}

	getAll(): ActionModel[] {
		return this.actionsRepository.findAll()
	}
}
<file_sep>import express, { Express } from 'express'
import cors from 'cors'
import helmet from 'helmet'
import { DogsService } from 'Src/pkg/dogs/dog.service'
import { createDogsHandler } from './handlers/dog.handler'
import { ActionsService } from 'Src/pkg/actions/actions.service'
import { createActionsHandler } from './handlers/actions.handler'
import { noSniffMiddleware } from 'Src/pkg/http/middleware/noSniff'
// eslint-disable-next-line max-len
import { morganLoggerMiddleware } from 'Src/pkg/http/middleware/morgan'
// eslint-disable-next-line max-len
import { errorHandlerMiddleware } from 'Src/pkg/http/middleware/error'
// eslint-disable-next-line max-len
import { routeNotFoundMiddleware } from 'Src/pkg/http/middleware/routeNotFound'

export function createRestServer(
	actionsService: ActionsService,
	dogsService: DogsService,
): Express {
	const app = express()

	app.use(cors())
	app.use(helmet())
	app.use(noSniffMiddleware)
	app.use(morganLoggerMiddleware)

	const router = express.Router()
	createDogsHandler(router, dogsService)
	createActionsHandler(router, actionsService)
	app.use('/', router)

	app.use(errorHandlerMiddleware)
	app.use(routeNotFoundMiddleware)

	return app
}
