├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── config ├── default.yaml └── production.yaml ├── docker-compose.production.yaml ├── docker-compose.yaml ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── @types │ └── augmentation.ts ├── __tests__ │ ├── __snapshots__ │ │ └── simple.test.ts.snap │ └── simple.test.ts ├── index.ts └── modules │ ├── db │ ├── index.ts │ └── models │ │ └── vehicle.ts │ └── routes │ ├── error-thrower │ └── index.ts │ ├── status │ └── index.ts │ └── vehicles │ └── index.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | videos 4 | images -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | .env.test 60 | 61 | # parcel-bundler cache (https://parceljs.org/) 62 | .cache 63 | 64 | # next.js build output 65 | .next 66 | 67 | # nuxt.js build output 68 | .nuxt 69 | 70 | # vuepress build output 71 | .vuepress/dist 72 | 73 | # Serverless directories 74 | .serverless/ 75 | 76 | # FuseBox cache 77 | .fusebox/ 78 | 79 | # DynamoDB Local files 80 | .dynamodb/ 81 | # Logs 82 | logs 83 | *.log 84 | npm-debug.log* 85 | yarn-debug.log* 86 | yarn-error.log* 87 | 88 | # Runtime data 89 | pids 90 | *.pid 91 | *.seed 92 | *.pid.lock 93 | 94 | # Directory for instrumented libs generated by jscoverage/JSCover 95 | lib-cov 96 | 97 | # Coverage directory used by tools like istanbul 98 | coverage 99 | 100 | # nyc test coverage 101 | .nyc_output 102 | 103 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 104 | .grunt 105 | 106 | # Bower dependency directory (https://bower.io/) 107 | bower_components 108 | 109 | # node-waf configuration 110 | .lock-wscript 111 | 112 | # Compiled binary addons (https://nodejs.org/api/addons.html) 113 | build/Release 114 | 115 | # Dependency directories 116 | node_modules/ 117 | jspm_packages/ 118 | 119 | # TypeScript v1 declaration files 120 | typings/ 121 | 122 | # Optional npm cache directory 123 | .npm 124 | 125 | # Optional eslint cache 126 | .eslintcache 127 | 128 | # Optional REPL history 129 | .node_repl_history 130 | 131 | # Output of 'npm pack' 132 | *.tgz 133 | 134 | # Yarn Integrity file 135 | .yarn-integrity 136 | 137 | # dotenv environment variables file 138 | .env 139 | .env.test 140 | 141 | # parcel-bundler cache (https://parceljs.org/) 142 | .cache 143 | 144 | # next.js build output 145 | .next 146 | 147 | # nuxt.js build output 148 | .nuxt 149 | 150 | # vuepress build output 151 | .vuepress/dist 152 | 153 | # Serverless directories 154 | .serverless/ 155 | 156 | # FuseBox cache 157 | .fusebox/ 158 | 159 | # DynamoDB Local files 160 | .dynamodb/ 161 | 162 | # do not commit built version of the app 163 | dist -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:dubnium-alpine 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN npm install --no-progress --quiet 8 | RUN npm run build 9 | 10 | CMD [ "npm", "start"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify with typescript - production ready integration 2 | 3 | This repository is a demo part of the article [fastify with typescript - production ready integration](). 4 | 5 | You can have next grades of the integration covered here: 6 | - local development setup 7 | - integration with a database 8 | - testing 9 | - debugging 10 | - deployment 11 | - error logging -------------------------------------------------------------------------------- /config/default.yaml: -------------------------------------------------------------------------------- 1 | db: 2 | uri: "mongodb://localhost:27017/vehicles" -------------------------------------------------------------------------------- /config/production.yaml: -------------------------------------------------------------------------------- 1 | db: 2 | uri: "mongodb://mongodb:27017/vehicles" -------------------------------------------------------------------------------- /docker-compose.production.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | my-project-ts: 4 | build: . 5 | depends_on: 6 | - mongodb 7 | ports: 8 | - '3000:3000' 9 | environment: 10 | - NODE_ENV=production 11 | mongodb: 12 | image: mongo:3.6.0 13 | ports: 14 | - "27017:27017" 15 | volumes: 16 | - dbvolume:/data 17 | environment: { 18 | AUTH: "no" 19 | } 20 | volumes: 21 | dbvolume: {} 22 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | mongodb: 4 | image: mongo:3.6.0 5 | ports: 6 | - "27017:27017" 7 | volumes: 8 | - dbvolume:/data 9 | environment: { 10 | AUTH: "no" 11 | } 12 | volumes: 13 | dbvolume: {} 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rootDir: "src", 3 | testMatch: ["**/__tests__/**/*.test.(ts|tsx|js|jsx)"], 4 | verbose: false, 5 | clearMocks: true, 6 | resetModules: true, 7 | coveragePathIgnorePatterns: [ 8 | "/node_modules/", 9 | "/__fixtures__/", 10 | "/__tests__/", 11 | "/(__)?mock(s__)?/", 12 | "/__jest__/", 13 | ".?.min.js" 14 | ], 15 | moduleDirectories: ["node_modules", "src"], 16 | transform: { 17 | "^.+\\.(ts|tsx)$": "ts-jest" 18 | }, 19 | moduleFileExtensions: ["js", "jsx", "json", "ts"] 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-project-ts", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node ./dist/index.js", 8 | "test": "NODE_ENV=test npx jest --verbose --runInBand", 9 | "build": "rm -rf dist && tsc --sourceMap -p ./", 10 | "dev": "ts-node --files ./src/index.ts" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "@types/jest": "^23.3.12", 17 | "@types/mongodb": "^3.1.19", 18 | "@types/mongoose": "^5.3.8", 19 | "config": "^3.0.1", 20 | "fastify": "^1.13.4", 21 | "fastify-blipp": "^1.2.1", 22 | "fastify-plugin": "^1.4.0", 23 | "jest": "^23.6.0", 24 | "mongoose": "^5.4.4", 25 | "source-map-support": "^0.5.10", 26 | "ts-jest": "^23.10.5", 27 | "ts-node": "^7.0.1", 28 | "typescript": "^3.2.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/@types/augmentation.ts: -------------------------------------------------------------------------------- 1 | import * as fastify from "fastify"; 2 | import * as http from "http"; 3 | 4 | import { Db } from "../modules/db"; 5 | declare module "fastify" { 6 | export interface FastifyInstance< 7 | HttpServer = http.Server, 8 | HttpRequest = http.IncomingMessage, 9 | HttpResponse = http.ServerResponse 10 | > { 11 | blipp(): void; 12 | db: Db; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/simple.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`/status GET returns 200 1`] = ` 4 | Object { 5 | "date": Any, 6 | "works": true, 7 | } 8 | `; 9 | 10 | exports[`/status POST returns 404 1`] = `"{\\"statusCode\\":404,\\"error\\":\\"Not Found\\",\\"message\\":\\"Not Found\\"}"`; 11 | -------------------------------------------------------------------------------- /src/__tests__/simple.test.ts: -------------------------------------------------------------------------------- 1 | import * as fastify from "fastify"; 2 | import { Server, IncomingMessage, ServerResponse } from "http"; 3 | 4 | import statusRoutes from "../modules/routes/status"; 5 | 6 | describe("/status", () => { 7 | let server: fastify.FastifyInstance; 8 | 9 | beforeAll(() => {}); 10 | 11 | beforeEach(async () => { 12 | server = fastify({}); 13 | // eslint-disable-next-line global-require 14 | server.register(statusRoutes); 15 | await server.ready(); 16 | 17 | jest.clearAllMocks(); 18 | }); 19 | 20 | it("GET returns 200", async done => { 21 | const response = await server.inject({ method: "GET", url: "/status" }); 22 | expect(response.statusCode).toEqual(200); 23 | const payload: { date: Date; works: boolean } = JSON.parse( 24 | response.payload 25 | ); 26 | expect(payload).toMatchSnapshot({ date: expect.any(String), works: true }); 27 | 28 | done(); 29 | }); 30 | 31 | it("POST returns 404", async done => { 32 | const response = await server.inject({ method: "POST", url: "/status" }); 33 | expect(response.statusCode).toEqual(404); 34 | expect(response.payload).toMatchSnapshot(); 35 | 36 | done(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as sourceMapSupport from "source-map-support"; 2 | sourceMapSupport.install(); 3 | 4 | import * as fastify from "fastify"; 5 | import * as fastifyBlipp from "fastify-blipp"; 6 | import { Server, IncomingMessage, ServerResponse } from "http"; 7 | import * as config from "config"; 8 | import statusRoutes from "./modules/routes/status"; 9 | import vehiclesRoutes from "./modules/routes/vehicles"; 10 | import errorThrowerRoutes from "./modules/routes/error-thrower"; 11 | import db from "./modules/db"; 12 | 13 | const server: fastify.FastifyInstance< 14 | Server, 15 | IncomingMessage, 16 | ServerResponse 17 | > = fastify({logger:true}); 18 | 19 | server.register(fastifyBlipp); 20 | server.register(db, config.get('db')); 21 | server.register(vehiclesRoutes); 22 | server.register(statusRoutes); 23 | server.register(errorThrowerRoutes); 24 | 25 | const start = async () => { 26 | try { 27 | await server.listen(3000, "0.0.0.0"); 28 | server.blipp(); 29 | } catch (err) { 30 | console.log(err); 31 | server.log.error(err); 32 | process.exit(1); 33 | } 34 | }; 35 | 36 | process.on("uncaughtException", error => { 37 | console.error(error); 38 | }); 39 | process.on("unhandledRejection", error => { 40 | console.error(error); 41 | }); 42 | 43 | start(); 44 | -------------------------------------------------------------------------------- /src/modules/db/index.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "mongoose"; 2 | import * as Mongoose from "mongoose"; 3 | import { VehicleModel, Vehicle } from "./models/vehicle"; 4 | 5 | import * as fp from "fastify-plugin"; 6 | 7 | export interface Models { 8 | Vehicle: Model; 9 | } 10 | 11 | export interface Db { 12 | models: Models; 13 | } 14 | 15 | export default fp(async (fastify, opts: { uri: string }, next) => { 16 | Mongoose.connection.on("connected", () => { 17 | fastify.log.info({ actor: "MongoDB" }, "connected"); 18 | }); 19 | 20 | Mongoose.connection.on("disconnected", () => { 21 | fastify.log.error({ actor: "MongoDB" }, "disconnected"); 22 | }); 23 | 24 | await Mongoose.connect( 25 | opts.uri, 26 | { 27 | useNewUrlParser: true, 28 | keepAlive: 1 29 | } 30 | ); 31 | 32 | const models: Models = { 33 | Vehicle: Vehicle 34 | }; 35 | 36 | fastify.decorate("db", { models }); 37 | 38 | next(); 39 | }); 40 | -------------------------------------------------------------------------------- /src/modules/db/models/vehicle.ts: -------------------------------------------------------------------------------- 1 | import { Document, Schema, Model, model } from "mongoose"; 2 | 3 | export interface VehicleDocument extends Document { 4 | year: number; 5 | name: string; 6 | createdDate: Date; 7 | } 8 | 9 | export interface VehicleModel extends VehicleDocument {} 10 | 11 | export const VehicleSchema: Schema = new Schema( 12 | { 13 | year: Number, 14 | name: String, 15 | createdDate: Date 16 | }, 17 | { collection: "vehicles" } 18 | ); 19 | 20 | VehicleSchema.pre("save", async function() { 21 | this.createdDate = new Date(); 22 | }); 23 | 24 | export const Vehicle: Model = model( 25 | "Vehicle", 26 | VehicleSchema 27 | ); 28 | -------------------------------------------------------------------------------- /src/modules/routes/error-thrower/index.ts: -------------------------------------------------------------------------------- 1 | import * as fp from "fastify-plugin"; 2 | 3 | export default fp(async (server, opts, next) => { 4 | server.route({ 5 | url: "/error-thrower", 6 | method: ["GET"], 7 | handler: async (request, reply) => { 8 | throw new Error("Oh no, something bad happened, try to debug me"); 9 | return reply.send({ date: new Date(), works: true }); 10 | } 11 | }); 12 | next(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/modules/routes/status/index.ts: -------------------------------------------------------------------------------- 1 | import * as fp from "fastify-plugin"; 2 | 3 | export default fp(async (server, opts, next) => { 4 | server.route({ 5 | url: "/status", 6 | logLevel: "warn", 7 | method: ["GET", "HEAD"], 8 | handler: async (request, reply) => { 9 | return reply.send({ date: new Date(), works: true }); 10 | } 11 | }); 12 | next(); 13 | }); 14 | -------------------------------------------------------------------------------- /src/modules/routes/vehicles/index.ts: -------------------------------------------------------------------------------- 1 | import * as fp from "fastify-plugin"; 2 | 3 | export default fp(async (server, opts, next) => { 4 | server.get("/vehicles/:id", {}, async (request, reply) => { 5 | try { 6 | const _id = request.params.id; 7 | 8 | const vehicle = await server.db.models.Vehicle.findOne({ 9 | _id 10 | }); 11 | 12 | if (!vehicle) { 13 | return reply.send(404); 14 | } 15 | 16 | return reply.code(200).send(vehicle); 17 | } catch (error) { 18 | request.log.error(error); 19 | return reply.send(400); 20 | } 21 | }); 22 | 23 | server.post("/vehicles", {}, async (request, reply) => { 24 | try { 25 | const { Vehicle } = server.db.models; 26 | 27 | const vehicle = await Vehicle.create(request.body); 28 | 29 | return reply.code(201).send(vehicle); 30 | } catch (error) { 31 | request.log.error(error); 32 | return reply.send(500); 33 | } 34 | }); 35 | next(); 36 | }); 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "lib": ["es2016"], 5 | "paths": { 6 | "*": ["src/*"] 7 | }, 8 | "allowJs": true, 9 | "target": "es2016", 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "sourceMap": true, 13 | "outDir": "./dist" 14 | }, 15 | "include": ["./src/**/*.ts"], 16 | "exclude": ["node_modules", "./src/**/__tests__/*.ts"] 17 | } 18 | --------------------------------------------------------------------------------