├── .editorconfig ├── .env.example ├── .gitignore ├── .taprc ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── app.ts ├── contexts │ ├── login │ │ ├── core.ts │ │ ├── handler.ts │ │ └── types.ts │ └── register │ │ ├── core.ts │ │ ├── handler.ts │ │ └── types.ts ├── database │ └── index.ts ├── models │ ├── result.ts │ └── user.ts ├── plugins │ ├── mongoose.ts │ ├── queues.ts │ └── redis.ts ├── queues │ └── example.ts └── routes │ ├── README.md │ ├── auth │ ├── login │ │ └── index.ts │ └── register │ │ └── index.ts │ └── root.ts ├── test ├── helper.ts ├── plugins │ └── .gitkeep └── tsconfig.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | tab_width = 2 14 | 15 | [*.{json,txt,js}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.txt] 20 | end_of_line = crlf 21 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | 3 | MONGO_URI=mongodb://localhost:27017/fastify-example-dev 4 | 5 | REDIS_HOST=localhost 6 | REDIS_PORT=6379 7 | REDIS_PASSWORD='' 8 | 9 | JWT_SECRET=secret 10 | 11 | PORT=3000 12 | HOST=0.0.0.0 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # 0x 40 | profile-* 41 | 42 | # mac files 43 | .DS_Store 44 | 45 | # vim swap files 46 | *.swp 47 | 48 | # webstorm 49 | .idea 50 | 51 | # vscode 52 | .vscode 53 | *code-workspace 54 | 55 | # clinic 56 | profile* 57 | *clinic* 58 | *flamegraph* 59 | 60 | # generated code 61 | examples/typescript-server.js 62 | test/types/index.js 63 | 64 | # compiled app 65 | dist 66 | 67 | .env 68 | -------------------------------------------------------------------------------- /.taprc: -------------------------------------------------------------------------------- 1 | test-env: [ 2 | TS_NODE_FILES=true, 3 | TS_NODE_PROJECT=./test/tsconfig.json 4 | ] 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Christopher Ribeiro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fastify-template 2 | A Fastify template with TypeScript and a few patterns 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-template", 3 | "version": "1.1.1", 4 | "description": "This project was bootstrapped with Fastify-CLI.", 5 | "main": "app.ts", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "npm run build:ts && tsc -p test/tsconfig.json && tap --ts \"test/**/*.test.ts\"", 11 | "start": "npm run build:ts && fastify start -l info dist/app.js", 12 | "build:ts": "tsc", 13 | "watch:ts": "tsc -w", 14 | "dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"", 15 | "dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js" 16 | }, 17 | "keywords": [], 18 | "author": "Christopher Ribeiro ", 19 | "license": "MIT", 20 | "dependencies": { 21 | "@fastify/autoload": "^5.0.0", 22 | "@fastify/redis": "^6.1.1", 23 | "@fastify/sensible": "^5.0.0", 24 | "@types/bcrypt": "^5.0.0", 25 | "@types/jsonwebtoken": "^9.0.2", 26 | "bcrypt": "^5.1.0", 27 | "bullmq": "^4.6.0", 28 | "fastify": "^4.0.0", 29 | "fastify-cli": "^5.7.1", 30 | "fastify-plugin": "^4.0.0", 31 | "jsonwebtoken": "^9.0.0", 32 | "mongoose": "^7.5.2" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "^18.0.0", 36 | "@types/tap": "^15.0.5", 37 | "concurrently": "^7.0.0", 38 | "fastify-tsconfig": "^1.0.1", 39 | "tap": "^16.1.0", 40 | "ts-node": "^10.4.0", 41 | "typescript": "^4.5.4" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import AutoLoad, {AutoloadPluginOptions} from '@fastify/autoload'; 3 | import { FastifyPluginAsync } from 'fastify'; 4 | import './database'; 5 | 6 | export type AppOptions = { 7 | // Place your custom options for app below here. 8 | } & Partial; 9 | 10 | 11 | // Pass --options via CLI arguments in command to enable these options. 12 | const options: AppOptions = { 13 | } 14 | 15 | const app: FastifyPluginAsync = async ( 16 | fastify, 17 | opts 18 | ): Promise => { 19 | // Place here your custom code! 20 | 21 | // Do not touch the following lines 22 | 23 | // This loads all plugins defined in plugins 24 | // those should be support plugins that are reused 25 | // through your application 26 | void fastify.register(AutoLoad, { 27 | dir: join(__dirname, 'plugins'), 28 | options: opts 29 | }) 30 | 31 | // This loads all plugins defined in routes 32 | // define your routes in one of these 33 | void fastify.register(AutoLoad, { 34 | dir: join(__dirname, 'routes'), 35 | options: opts 36 | }) 37 | 38 | }; 39 | 40 | export default app; 41 | export { app, options } 42 | -------------------------------------------------------------------------------- /src/contexts/login/core.ts: -------------------------------------------------------------------------------- 1 | import { compareSync } from 'bcrypt'; 2 | import { sign } from 'jsonwebtoken' 3 | import { UserModel } from "../../models/user"; 4 | import { LoginInput, LoginResult } from "./types"; 5 | import { Result } from '../../models/result'; 6 | 7 | export async function login(data: LoginInput): Promise> { 8 | const user = await UserModel.findOne({ email: data.email }); 9 | if (!user) { 10 | return { error: 'E-mail or password is incorrect' } 11 | } 12 | 13 | const match = compareSync(data.password, user.password); 14 | if (!match) { 15 | return { error: 'E-mail or password is incorrect' } 16 | } 17 | 18 | const token = sign({ _id: user._id }, process.env.JWT_SECRET as string, { expiresIn: '1d' }); 19 | 20 | return { 21 | data: { 22 | token, 23 | user: { 24 | _id: user._id.toString(), 25 | name: user.name, 26 | email: user.email, 27 | } 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/contexts/login/handler.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" 2 | import { LoginInput } from "./types"; 3 | import { login } from "./core"; 4 | 5 | async function handler(fastify: FastifyInstance, request: FastifyRequest, reply: FastifyReply) { 6 | const { error, data } = await login(request.body as LoginInput) 7 | 8 | if (error) { 9 | reply.status(401).send({ message: error }); 10 | return; 11 | } 12 | 13 | 14 | fastify.redis.set(data!.token, JSON.stringify(data!.user), 'EX', 86400); 15 | fastify.queues.example.add('example', { example: 42 }) 16 | reply.status(200).send(data); 17 | } 18 | 19 | export default handler; 20 | -------------------------------------------------------------------------------- /src/contexts/login/types.ts: -------------------------------------------------------------------------------- 1 | import { PublicUser } from "../../models/user"; 2 | 3 | export interface LoginInput { 4 | email: string; 5 | password: string; 6 | } 7 | 8 | export interface LoginResult { 9 | token: string; 10 | user: PublicUser; 11 | } 12 | -------------------------------------------------------------------------------- /src/contexts/register/core.ts: -------------------------------------------------------------------------------- 1 | import { hashSync } from 'bcrypt'; 2 | import { PublicUser, UserModel } from "../../models/user"; 3 | import { RegisterInput } from "./types"; 4 | import { Result } from '../../models/result'; 5 | 6 | export async function register(data: RegisterInput): Promise> { 7 | const user = await UserModel.findOne({ email: data.email }); 8 | if (user) { 9 | return { error: 'E-mail alrady in use' } 10 | } 11 | 12 | const hash = hashSync(data.password, 30); 13 | const newUser = await UserModel.create({ 14 | name: data.name, 15 | email: data.email, 16 | password: hash, 17 | }); 18 | await newUser.save(); 19 | 20 | return { 21 | data: { 22 | _id: newUser._id.toString(), 23 | name: newUser.name, 24 | email: newUser.email, 25 | }, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/contexts/register/handler.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify" 2 | import { RegisterInput } from "./types"; 3 | import { register } from "./core"; 4 | 5 | async function handler(fastify: FastifyInstance, request: FastifyRequest, reply: FastifyReply) { 6 | const { error, data } = await register(request.body as RegisterInput) 7 | 8 | if (error) { 9 | reply.status(400).send({ message: error }); 10 | return; 11 | } 12 | 13 | reply.status(200).send(data); 14 | } 15 | 16 | export default handler; 17 | -------------------------------------------------------------------------------- /src/contexts/register/types.ts: -------------------------------------------------------------------------------- 1 | export interface RegisterInput { 2 | name: string; 3 | email: string; 4 | password: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/database/index.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | mongoose.connect(process.env.MONGO_URI as string); 4 | 5 | const db = mongoose.connection.useDb(process.env.MONGO_DB as string); 6 | 7 | db.on('error', () => { 8 | console.error.bind(console, 'MongoDB connection error') 9 | process.exit(1) 10 | }); 11 | db.once('open', () => {}); 12 | 13 | export default db; 14 | -------------------------------------------------------------------------------- /src/models/result.ts: -------------------------------------------------------------------------------- 1 | export interface Result { 2 | error?: string; 3 | data?: T; 4 | } 5 | -------------------------------------------------------------------------------- /src/models/user.ts: -------------------------------------------------------------------------------- 1 | import { Schema, ObjectId } from "mongoose"; 2 | import db from "../database"; 3 | 4 | export interface User { 5 | _id: ObjectId; 6 | name: string; 7 | email: string; 8 | password: string; 9 | createdAt: number; 10 | updatedAt: number; 11 | } 12 | 13 | export interface PublicUser { 14 | _id: string; 15 | name: string; 16 | email: string; 17 | } 18 | 19 | export const UserSchema = new Schema({ 20 | name: { type: String, required: true }, 21 | email: { type: String, required: true, unique: true }, 22 | password: { type: String, required: true }, 23 | createdAt: { type: Number, default: Date.now }, 24 | updatedAt: { type: Number, default: Date.now }, 25 | }); 26 | 27 | export const UserModel = db.model("User", UserSchema); 28 | -------------------------------------------------------------------------------- /src/plugins/mongoose.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | import db from '../database' 3 | 4 | /** 5 | * This plugin gracefully closes the database connection 6 | * when the server is running 7 | * 8 | */ 9 | export default fp(async (fastify) => { 10 | fastify.addHook('onClose', async (_, done) => { 11 | await db.close() 12 | done() 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /src/plugins/queues.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | import { Job, Queue, Worker } from 'bullmq' 3 | import IORedis, { RedisOptions } from 'ioredis' 4 | import { readdirSync } from 'fs' 5 | import { join, basename, extname } from 'path' 6 | import { FastifyInstance } from 'fastify' 7 | 8 | export interface ProcessorInjectedContext { 9 | job: Job 10 | fastify: FastifyInstance 11 | } 12 | 13 | /** 14 | * This plugin adds Bull queues to the Fastify instance 15 | */ 16 | export default fp(async (fastify, opts) => { 17 | const connection = new IORedis({ 18 | host: process.env.REDIS_HOST, 19 | port: Number(process.env.REDIS_PORT), 20 | password: process.env.REDIS_PASSWORD 21 | } as RedisOptions) 22 | 23 | const queuesPath = join(__dirname, '../queues') 24 | const queueFiles = readdirSync(queuesPath) 25 | 26 | const queues: Record = {} 27 | 28 | for (const file of queueFiles) { 29 | if (!file.endsWith('.js')) { 30 | 31 | continue 32 | } 33 | 34 | const queueName = basename(file, extname(file)) 35 | const queue = new Queue( 36 | queueName, 37 | { 38 | connection, 39 | defaultJobOptions: { 40 | attempts: 3, 41 | backoff: { 42 | type: 'exponential', 43 | delay: 1000, 44 | }, 45 | removeOnComplete: true 46 | }, 47 | }) 48 | 49 | const processorPath = join(queuesPath, file) 50 | const processor = require(processorPath).default 51 | 52 | new Worker(queueName, async (job) => { 53 | await processor({ job, fastify } as ProcessorInjectedContext) 54 | }, { connection }) 55 | 56 | queues[queueName] = queue 57 | } 58 | 59 | fastify.decorate('queues', queues) 60 | }) 61 | 62 | // Update Fastify namespace to include queues 63 | declare module 'fastify' { 64 | interface FastifyInstance { 65 | queues: { 66 | [key: string]: Queue 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/plugins/redis.ts: -------------------------------------------------------------------------------- 1 | import fp from 'fastify-plugin' 2 | import fastifyRedis, { FastifyRedisPluginOptions, FastifyRedis } from '@fastify/redis' 3 | 4 | /** 5 | * This plugin adds a redis client to the fastify instance 6 | */ 7 | export default fp(async (fastify, opts) => { 8 | fastify.register(fastifyRedis, { 9 | host: process.env.REDIS_HOST, 10 | port: Number(process.env.REDIS_PORT), 11 | password: process.env.REDIS_PASSWORD, 12 | }) 13 | }) 14 | 15 | // update fastify namespace to include redis 16 | declare module 'fastify' { 17 | interface FastifyInstance { 18 | redis: FastifyRedis 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/queues/example.ts: -------------------------------------------------------------------------------- 1 | import { ProcessorInjectedContext } from "../plugins/queues" 2 | 3 | export interface ExampleJob { 4 | result: 42 5 | } 6 | 7 | export default async function ({ job, fastify }: ProcessorInjectedContext) { 8 | console.log(job.data.result) 9 | console.log(fastify.version) 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/README.md: -------------------------------------------------------------------------------- 1 | # Routes Folder 2 | 3 | Routes define endpoints within your application. Fastify provides an 4 | easy path to a microservice architecture, in the future you might want 5 | to independently deploy some of those. 6 | 7 | In this folder you should define all the routes that define the endpoints 8 | of your web application. 9 | Each service is a [Fastify 10 | plugin](https://www.fastify.io/docs/latest/Reference/Plugins/), it is 11 | encapsulated (it can have its own independent plugins) and it is 12 | typically stored in a file; be careful to group your routes logically, 13 | e.g. all `/users` routes in a `users.js` file. We have added 14 | a `root.js` file for you with a '/' root added. 15 | 16 | If a single file become too large, create a folder and add a `index.js` file there: 17 | this file must be a Fastify plugin, and it will be loaded automatically 18 | by the application. You can now add as many files as you want inside that folder. 19 | In this way you can create complex routes within a single monolith, 20 | and eventually extract them. 21 | 22 | If you need to share functionality between routes, place that 23 | functionality into the `plugins` folder, and share it via 24 | [decorators](https://www.fastify.io/docs/latest/Reference/Decorators/). 25 | -------------------------------------------------------------------------------- /src/routes/auth/login/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginAsync, RouteOptions } from "fastify" 2 | import handler from "../../../contexts/login/handler" 3 | 4 | const example: FastifyPluginAsync = async (fastify, opts): Promise => { 5 | const options: RouteOptions = { 6 | method: 'POST', 7 | url: '/', 8 | schema: { 9 | body: { 10 | type: 'object', 11 | properties: { 12 | email: { type: 'string' }, 13 | password: { type: 'string' }, 14 | }, 15 | required: ['email', 'password'], 16 | }, 17 | }, 18 | handler: async function (request, reply) { 19 | return handler(fastify, request, reply) 20 | } 21 | } 22 | fastify.route(options) 23 | } 24 | 25 | export default example; 26 | -------------------------------------------------------------------------------- /src/routes/auth/register/index.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginAsync, RouteOptions } from "fastify" 2 | import handler from "../../../contexts/register/handler" 3 | 4 | const example: FastifyPluginAsync = async (fastify, opts): Promise => { 5 | const options: RouteOptions = { 6 | method: 'POST', 7 | url: '/', 8 | schema: { 9 | body: { 10 | type: 'object', 11 | properties: { 12 | name: { type: 'string' }, 13 | email: { type: 'string' }, 14 | password: { type: 'string' }, 15 | }, 16 | required: ['name', 'email', 'password'], 17 | }, 18 | }, 19 | handler: async function (request, reply) { 20 | return handler(fastify, request, reply) 21 | } 22 | } 23 | fastify.route(options) 24 | } 25 | 26 | export default example; 27 | -------------------------------------------------------------------------------- /src/routes/root.ts: -------------------------------------------------------------------------------- 1 | import { FastifyPluginAsync } from 'fastify' 2 | 3 | const root: FastifyPluginAsync = async (fastify, opts): Promise => { 4 | fastify.get('/', async function (request, reply) { 5 | return { root: true } 6 | }) 7 | } 8 | 9 | export default root; 10 | -------------------------------------------------------------------------------- /test/helper.ts: -------------------------------------------------------------------------------- 1 | // This file contains code that we reuse between our tests. 2 | const helper = require('fastify-cli/helper.js') 3 | import * as path from 'path' 4 | import * as tap from 'tap'; 5 | 6 | export type Test = typeof tap['Test']['prototype']; 7 | 8 | const AppPath = path.join(__dirname, '..', 'src', 'app.ts') 9 | 10 | // Fill in this config with all the configurations 11 | // needed for testing the application 12 | async function config () { 13 | return {} 14 | } 15 | 16 | // Automatically build and tear down our instance 17 | async function build (t: Test) { 18 | // you can set all the options supported by the fastify CLI command 19 | const argv = [AppPath] 20 | 21 | // fastify-plugin ensures that all decorators 22 | // are exposed for testing purposes, this is 23 | // different from the production setup 24 | const app = await helper.build(argv, await config()) 25 | 26 | // Tear down our app after we are done 27 | t.teardown(() => void app.close()) 28 | 29 | return app 30 | } 31 | 32 | export { 33 | config, 34 | build 35 | } 36 | -------------------------------------------------------------------------------- /test/plugins/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChristoPy/fastify-template/1f1ca5ba64804e5c2076ef2783e426295eb67adf/test/plugins/.gitkeep -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "noEmit": true 6 | }, 7 | "include": ["../src/**/*.ts", "**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "fastify-tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": true 6 | }, 7 | "include": ["src/**/*.ts"] 8 | } 9 | --------------------------------------------------------------------------------