├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc.json
├── .prettierrc
├── .swcrc
├── Dockerfile
├── README.md
├── env.example
├── nodemon.json
├── package-lock.json
├── package.json
├── src
├── app.ts
├── components
│ ├── auth
│ │ ├── auth.controller.ts
│ │ ├── auth.interface.ts
│ │ ├── auth.route.ts
│ │ ├── auth.schema.ts
│ │ └── auth.service.ts
│ ├── default
│ │ ├── index.controller.ts
│ │ └── index.route.ts
│ └── user
│ │ ├── user.controller.ts
│ │ ├── user.interface.ts
│ │ ├── user.route.ts
│ │ ├── user.schema.ts
│ │ └── user.service.ts
├── config
│ └── index.ts
├── constants
│ └── constants.ts
├── exceptions
│ └── error.ts
├── interfaces
│ └── routes.interface.ts
├── plugins
│ ├── authentication.ts
│ ├── initializeRoute.ts
│ └── swagger.ts
├── prisma
│ ├── migrations
│ │ ├── 20230718134925_init
│ │ │ └── migration.sql
│ │ ├── 20230721125416_added_password_to_user
│ │ │ └── migration.sql
│ │ ├── 20230721135627_added_char_limit_to_password
│ │ │ └── migration.sql
│ │ └── migration_lock.toml
│ ├── schema.prisma
│ └── seed.ts
├── server.ts
├── types
│ ├── fastify.d.ts
│ └── index.d.ts
└── utils
│ ├── prisma.ts
│ ├── schemaErrorFormatter.ts
│ └── validateEnv.ts
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | .vscode
3 | /node_modules
4 |
5 | # code formatter
6 | .eslintrc
7 | .eslintignore
8 | .editorconfig
9 | .huskyrc
10 | .lintstagedrc.json
11 | .prettierrc
12 |
13 | # test
14 | jest.config.js
15 |
16 | # docker
17 | Dockerfile
18 | docker-compose.yml
19 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /dist
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "@typescript-eslint/parser",
3 | "extends": [
4 | "airbnb-base",
5 | "airbnb-typescript/base",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:import/recommended",
8 | "prettier"
9 | ],
10 | "parserOptions": {
11 | "ecmaVersion": 2022,
12 | "sourceType": "module",
13 | "project": "tsconfig.json"
14 | },
15 | "rules": {
16 | "fp/no-events": 0,
17 | "import/no-cycle": 0,
18 | "import/no-named-as-default-member": 0,
19 | "import/no-unresolved": 0,
20 | "import/prefer-default-export": 0,
21 | "sort-imports": [
22 | 1,
23 | {
24 | "ignoreCase": true,
25 | "ignoreDeclarationSort": true
26 | }
27 | ],
28 | "linebreak-style": 0,
29 | "no-continue": 0,
30 | "no-extra-parens": 0,
31 | "no-restricted-syntax": 0,
32 | "no-unused-expressions": [
33 | 2,
34 | {
35 | "allowTaggedTemplates": true
36 | }
37 | ],
38 | "require-await": 0,
39 | "unicorn/numeric-separators-style": 0,
40 | "unicorn/prevent-abbreviations": 0,
41 | "array-bracket-spacing": ["error", "never"],
42 | "comma-dangle": ["error", "never"],
43 | "comma-spacing": [
44 | "error",
45 | {
46 | "before": false,
47 | "after": true
48 | }
49 | ],
50 | "curly": ["error", "all"],
51 | "eol-last": ["error"],
52 | "keyword-spacing": ["error"],
53 | "max-len": [
54 | "error",
55 | {
56 | "code": 120,
57 | "tabWidth": 2,
58 | "ignoreComments": true,
59 | "ignoreStrings": true,
60 | "ignoreRegExpLiterals": true,
61 | "ignoreUrls": true
62 | }
63 | ],
64 | "default-param-last": ["error"],
65 | "no-else-return": ["error"],
66 | "no-mixed-spaces-and-tabs": ["error"],
67 | "no-multiple-empty-lines": ["error"],
68 | "no-spaced-func": ["error"],
69 | "no-trailing-spaces": ["error"],
70 | "no-undef": ["error"],
71 | "no-unexpected-multiline": ["error"],
72 | "no-empty-function": ["error"],
73 | "no-useless-catch": 0,
74 | "no-unused-vars": [
75 | "error",
76 | {
77 | "args": "after-used",
78 | "vars": "all",
79 | "ignoreRestSiblings": true
80 | }
81 | ],
82 | "quotes": [
83 | "error",
84 | "single",
85 | {
86 | "allowTemplateLiterals": true,
87 | "avoidEscape": true
88 | }
89 | ],
90 | "semi": ["error", "always"],
91 | "space-before-blocks": ["error", "always"],
92 | "space-in-parens": ["error", "never"],
93 | "space-unary-ops": [
94 | "error",
95 | {
96 | "nonwords": false,
97 | "overrides": {}
98 | }
99 | ],
100 | // ECMAScript 6 rules
101 | "arrow-body-style": [
102 | "error",
103 | "as-needed",
104 | {
105 | "requireReturnForObjectLiteral": false
106 | }
107 | ],
108 | "arrow-parens": ["error", "always"],
109 | "arrow-spacing": [
110 | "error",
111 | {
112 | "after": true,
113 | "before": true
114 | }
115 | ],
116 | "no-class-assign": ["error"],
117 | "no-const-assign": ["error"],
118 | "no-dupe-class-members": ["error"],
119 | "no-duplicate-imports": ["error"],
120 | "no-useless-rename": ["error"],
121 | "no-var": ["error"],
122 | "object-shorthand": [
123 | "error",
124 | "always",
125 | {
126 | "avoidQuotes": true,
127 | "ignoreConstructors": false
128 | }
129 | ],
130 | "prefer-arrow-callback": [
131 | "error",
132 | {
133 | "allowNamedFunctions": false,
134 | "allowUnboundThis": true
135 | }
136 | ],
137 | "prefer-const": ["error"],
138 | "prefer-rest-params": ["error"],
139 | "prefer-template": ["error"],
140 | "template-curly-spacing": ["error", "never"]
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 | .pnpm-debug.log*
9 |
10 | # Diagnostic reports (https://nodejs.org/api/report.html)
11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
12 |
13 | # Runtime data
14 | pids
15 | *.pid
16 | *.seed
17 | *.pid.lock
18 |
19 | # Directory for instrumented libs generated by jscoverage/JSCover
20 | lib-cov
21 |
22 | # Coverage directory used by tools like istanbul
23 | coverage
24 | *.lcov
25 |
26 | # nyc test coverage
27 | .nyc_output
28 |
29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
30 | .grunt
31 |
32 | # Bower dependency directory (https://bower.io/)
33 | bower_components
34 |
35 | # node-waf configuration
36 | .lock-wscript
37 |
38 | # Compiled binary addons (https://nodejs.org/api/addons.html)
39 | build/Release
40 |
41 | # Dependency directories
42 | node_modules/
43 | jspm_packages/
44 |
45 | # Snowpack dependency directory (https://snowpack.dev/)
46 | web_modules/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Optional stylelint cache
58 | .stylelintcache
59 |
60 | # Microbundle cache
61 | .rpt2_cache/
62 | .rts2_cache_cjs/
63 | .rts2_cache_es/
64 | .rts2_cache_umd/
65 |
66 | # Optional REPL history
67 | .node_repl_history
68 |
69 | # Output of 'npm pack'
70 | *.tgz
71 |
72 | # Yarn Integrity file
73 | .yarn-integrity
74 |
75 | # dotenv environment variable files
76 | .env
77 | .env.development.local
78 | .env.test.local
79 | .env.production.local
80 | .env.local
81 |
82 | # parcel-bundler cache (https://parceljs.org/)
83 | .cache
84 | .parcel-cache
85 |
86 | # Next.js build output
87 | .next
88 | out
89 |
90 | # Nuxt.js build / generate output
91 | .nuxt
92 | dist
93 |
94 | # Gatsby files
95 | .cache/
96 | # Comment in the public line in if your project uses Gatsby and not Next.js
97 | # https://nextjs.org/blog/next-9-1#public-directory-support
98 | # public
99 |
100 | # vuepress build output
101 | .vuepress/dist
102 |
103 | # vuepress v2.x temp and cache directory
104 | .temp
105 | .cache
106 |
107 | # Docusaurus cache and generated files
108 | .docusaurus
109 |
110 | # Serverless directories
111 | .serverless/
112 |
113 | # FuseBox cache
114 | .fusebox/
115 |
116 | # DynamoDB Local files
117 | .dynamodb/
118 |
119 | # TernJS port file
120 | .tern-port
121 |
122 | # Stores VSCode versions used for testing VSCode extensions
123 | .vscode-test
124 |
125 | # yarn v2
126 | .yarn/cache
127 | .yarn/unplugged
128 | .yarn/build-state.yml
129 | .yarn/install-state.gz
130 | .pnp.*
131 |
132 | .vscode
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npm run lint
5 |
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "*.ts": [
3 | "npm run lint"
4 | ]
5 | }
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "semi": true,
6 | "singleQuote": true,
7 | "quoteProps": "as-needed",
8 | "jsxSingleQuote": false,
9 | "trailingComma": "none",
10 | "bracketSpacing": true,
11 | "jsxBracketSameLine": false,
12 | "arrowParens": "always"
13 | }
14 |
--------------------------------------------------------------------------------
/.swcrc:
--------------------------------------------------------------------------------
1 | {
2 | "jsc": {
3 | "parser": {
4 | "syntax": "typescript",
5 | "tsx": false,
6 | "dynamicImport": true,
7 | "decorators": true
8 | },
9 | "transform": {
10 | "legacyDecorator": true,
11 | "decoratorMetadata": true
12 | },
13 | "target": "es2022",
14 | "externalHelpers": false,
15 | "keepClassNames": true,
16 | "loose": false,
17 | "minify": {
18 | "compress": false,
19 | "mangle": false
20 | },
21 | "baseUrl": "src",
22 | "paths": {
23 | "@/*": ["*"],
24 | "@config": ["config"],
25 | "@interfaces/*": ["interfaces/*"],
26 | "@utils/*": ["utils/*"],
27 | "@constants/*": ["constants/*"],
28 | "@plugins/*": ["plugins/*"],
29 | "@components/*": ["components/*"],
30 | "@exceptions/*": ["exceptions/*"]
31 | }
32 | },
33 | "module": {
34 | "type": "commonjs"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18-alpine
2 |
3 |
4 | # Set the working directory in the container
5 | WORKDIR /app
6 |
7 | # Copy the package.json and package-lock.json files to the container
8 | COPY package*.json ./
9 |
10 | RUN npm install
11 |
12 | # Copy the remaining files to the container
13 | COPY . .
14 |
15 | # Generate prisma client
16 | RUN npm run prisma:generate:prod
17 |
18 | RUN npm run prisma:migrate:prod
19 |
20 | ENV NODE_ENV=production
21 |
22 | EXPOSE 3001
23 |
24 | CMD ["npm", "run", "start"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Fastify Typescript Starter
4 |
5 |
6 |
7 | Fastify Rest API Boilerplate Using TypeScript
8 |
9 |
10 |
11 | ## Features:
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | ## How to use
53 |
54 | ### 1. Clone this repo & install dependencies
55 |
56 | Install Node dependencies:
57 |
58 | `npm install`
59 |
60 | ### 2. Create env files
61 |
62 | Create the env files for dev, test and prod,
63 | refer env.example
64 |
65 | ```sh
66 | .env.development.local
67 | .env.test.local
68 | .env.production.local
69 | ```
70 |
71 | ### 2. Set up the database
72 |
73 | The boilerplate uses [Postgres database](https://www.postgresql.org/).
74 |
75 | and update the db credentials in the env file
76 |
77 | ### 3. Generate Prisma Client
78 |
79 | Run the following command to generate [Prisma Client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/generating-prisma-client):
80 |
81 | ```sh
82 | npm run prisma:generate:dev
83 | ```
84 |
85 | ### 4. Migrate Schema
86 |
87 | Run the following command to migrate [Prisma Migrate](https://www.prisma.io/docs/concepts/components/prisma-migrate)
88 |
89 | ```sh
90 | npm run prisma:migrate:dev
91 | ```
92 |
93 | ### 5. Start the server
94 |
95 | Launch your server with this command:
96 |
97 | ```sh
98 | npm run dev
99 | ```
100 |
101 | ### Swagger Endpoint:
102 |
103 | http://localhost:3001/api-docs
104 |
--------------------------------------------------------------------------------
/env.example:
--------------------------------------------------------------------------------
1 | # PORT
2 | PORT = 3001
3 |
4 | API_VERSION = v1
5 |
6 | APP_ENV = dev
7 | #Urls
8 | SERVER_URL = http://localhost:3001
9 |
10 | # DATABASE
11 | DATABASE_URL = postgresql://username:pass@localhost:5432/db_name?schema=public
12 |
13 | # LOG
14 | LOG_FORMAT = dev
15 | LOG_DIR = ../logs
16 |
17 | # CORS
18 | ORIGIN = http://localhost:5173
19 |
20 | CREDENTIALS = true
21 |
22 | SECRET_KEY = secret for jwt
--------------------------------------------------------------------------------
/nodemon.json:
--------------------------------------------------------------------------------
1 | {
2 | "watch": ["src", ".env", ".env.development.local"],
3 | "ext": "js,ts,json",
4 | "ignore": ["src/logs/*", "src/**/*.{spec,test}.ts"],
5 | "exec": "ts-node --swc -r tsconfig-paths/register --transpile-only src/server.ts"
6 | }
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fastify-typescript-starter",
3 | "version": "2.1.0",
4 | "description": "fastify + typescript starter",
5 | "main": "index.js",
6 | "keywords": [
7 | "node.js",
8 | "boilerplate",
9 | "fastify",
10 | "typescript",
11 | "prisma",
12 | "prettier",
13 | "eslint",
14 | "nodemon",
15 | "docker",
16 | "swagger",
17 | "swc"
18 | ],
19 | "scripts": {
20 | "start": "npm run build:prod && cross-env NODE_ENV=production node dist/server.js",
21 | "dev": "cross-env NODE_ENV=development nodemon",
22 | "build:prod": "cross-env NODE_ENV=production swc src -d dist --source-maps --copy-files",
23 | "build:dev": "cross-env NODE_ENV=development swc src -d dist --source-maps --copy-files",
24 | "prisma:generate:dev": "dotenv -e .env.development.local -- npx prisma generate",
25 | "prisma:generate:prod": "dotenv -e .env.production.local -- npx prisma generate",
26 | "prisma:migrate:dev": "dotenv -e .env.development.local -- npx prisma migrate dev",
27 | "prisma:migrate:prod": "dotenv -e .env.production.local -- npx prisma migrate deploy",
28 | "prisma:seed:dev": "cross-env NODE_ENV=dev dotenv -e .env.development.local -- npx prisma db seed",
29 | "prisma:seed:prod": "cross-env NODE_ENV=prod dotenv -e .env.production.local -- npx prisma db seed",
30 | "prisma:reset:dev": "dotenv -e .env.development.local -- npx prisma migrate reset --skip-seed",
31 | "prisma:reset:prod": "dotenv -e .env.production.local -- npx prisma migrate reset --skip-seed",
32 | "check-types": "tsc --noemit",
33 | "lint": "eslint --ignore-path .gitignore --ext .ts src/ && npm run check-types",
34 | "lint:fix": "npm run lint --fix",
35 | "prepare": "husky install"
36 | },
37 | "prisma": {
38 | "schema": "src/prisma/schema.prisma",
39 | "seed": "ts-node src/prisma/seed.ts"
40 | },
41 | "author": "TheB1gFatPanda",
42 | "license": "MIT",
43 | "dependencies": {
44 | "@fastify/compress": "6.4.0",
45 | "@fastify/cors": "8.3.0",
46 | "@fastify/env": "4.2.0",
47 | "@fastify/helmet": "11.0.0",
48 | "@fastify/jwt": "7.2.0",
49 | "@fastify/swagger": "8.8.0",
50 | "@fastify/swagger-ui": "1.9.3",
51 | "@fastify/type-provider-typebox": "3.4.0",
52 | "@prisma/client": "5.1.1",
53 | "@sinclair/typebox": "0.30.2",
54 | "ajv-errors": "3.0.0",
55 | "bcrypt": "5.1.0",
56 | "dotenv": "16.3.1",
57 | "dotenv-cli": "7.2.1",
58 | "fastify": "4.21.0",
59 | "fastify-plugin": "4.5.1"
60 | },
61 | "devDependencies": {
62 | "@swc/cli": "0.1.62",
63 | "@swc/core": "1.3.74",
64 | "@swc/helpers": "0.5.1",
65 | "@types/bcrypt": "5.0.0",
66 | "@types/node": "20.4.6",
67 | "@typescript-eslint/eslint-plugin": "6.2.1",
68 | "@typescript-eslint/parser": "6.2.1",
69 | "cross-env": "7.0.3",
70 | "eslint": "8.46.0",
71 | "eslint-config-airbnb-typescript": "17.1.0",
72 | "eslint-config-prettier": "8.10.0",
73 | "eslint-plugin-import": "2.28.0",
74 | "husky": "8.0.3",
75 | "lint-staged": "13.2.3",
76 | "nodemon": "3.0.1",
77 | "prettier": "3.0.1",
78 | "prisma": "5.1.1",
79 | "source-map-support": "0.5.21",
80 | "ts-node": "10.9.1",
81 | "tsc-alias": "1.8.7",
82 | "tsconfig-paths": "4.2.0",
83 | "typescript": "5.1.6"
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import Fastify, { FastifyError, FastifyInstance } from 'fastify';
2 | import ajvErrors from 'ajv-errors';
3 | import fastifyHelmet from '@fastify/helmet';
4 | import fastifyCors from '@fastify/cors';
5 | import fastifyCompress from '@fastify/compress';
6 | import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
7 | import fastifyJwt from '@fastify/jwt';
8 | import { API_VERSION, CREDENTIALS, NODE_ENV, ORIGIN, PORT, SECRET_KEY } from '@config';
9 | import fastifyEnv from '@fastify/env';
10 |
11 | import { initializeRoutes } from '@plugins/initializeRoute';
12 | import { authentication } from '@plugins/authentication';
13 | import { initSwagger } from '@plugins/swagger';
14 |
15 | import { schemaErrorFormatter } from '@utils/schemaErrorFormatter';
16 |
17 | import { schema } from '@utils/validateEnv';
18 |
19 | class App {
20 | public app: FastifyInstance;
21 |
22 | public env: string;
23 |
24 | public port: number;
25 |
26 | constructor() {
27 | this.app = Fastify({
28 | schemaErrorFormatter,
29 | ajv: {
30 | customOptions: {
31 | coerceTypes: false,
32 | allErrors: true
33 | },
34 | plugins: [ajvErrors]
35 | },
36 | logger: true
37 | }).withTypeProvider();
38 |
39 | this.env = NODE_ENV ?? 'development';
40 | this.port = Number(PORT) ?? 3001;
41 |
42 | this.init();
43 | }
44 |
45 | public async listen() {
46 | try {
47 | await this.app.listen({ port: this.port });
48 | } catch (err) {
49 | this.app.log.error(err);
50 | process.exit(1);
51 | }
52 | }
53 |
54 | public getServer() {
55 | return this.app;
56 | }
57 |
58 | private init() {
59 | this.initializePlugins();
60 | this.initializeRoutes();
61 | this.initializeErrorHandling();
62 | }
63 |
64 | private initializePlugins() {
65 | this.app.register(fastifyEnv, { dotenv: true, schema });
66 | this.app.register(fastifyCors, { origin: ORIGIN, credentials: CREDENTIALS === 'true' });
67 | this.app.register(fastifyHelmet);
68 | this.app.register(fastifyCompress);
69 | this.app.register(fastifyJwt, { secret: SECRET_KEY ?? '' });
70 | this.app.register(authentication);
71 | this.app.register(initSwagger);
72 | }
73 |
74 | private initializeRoutes() {
75 | this.app.register(initializeRoutes, { prefix: `api/${API_VERSION}` });
76 | }
77 |
78 | private initializeErrorHandling() {
79 | this.app.setErrorHandler((error: FastifyError, request, reply) => {
80 | const status: number = error.statusCode ?? 500;
81 | const message: string = status === 500 ? 'Something went wrong' : error.message ?? 'Something went wrong';
82 |
83 | this.app.log.error(`[${request.method}] ${request.url} >> StatusCode:: ${status}, Message:: ${message}`);
84 |
85 | return reply.status(status).send({ error: true, message });
86 | });
87 | }
88 | }
89 |
90 | export default App;
91 |
--------------------------------------------------------------------------------
/src/components/auth/auth.controller.ts:
--------------------------------------------------------------------------------
1 | import { FastifyReply, FastifyRequest } from 'fastify';
2 |
3 | import { LoginUser } from '@components/auth/auth.interface';
4 |
5 | import AuthService from '@components/auth/auth.service';
6 |
7 | class AuthController {
8 | public authService = new AuthService();
9 |
10 | public login = async (req: FastifyRequest<{ Body: LoginUser }>, reply: FastifyReply) => {
11 | const { email, password } = req.body;
12 |
13 | const data = await this.authService.LoginUser({ email, password }, reply);
14 |
15 | return { data, message: 'login' };
16 | };
17 | }
18 |
19 | export default AuthController;
20 |
--------------------------------------------------------------------------------
/src/components/auth/auth.interface.ts:
--------------------------------------------------------------------------------
1 | import { Static } from '@fastify/type-provider-typebox';
2 |
3 | import { LoginUserBody } from '@components/auth/auth.schema';
4 |
5 | export type LoginUser = Static;
6 |
--------------------------------------------------------------------------------
/src/components/auth/auth.route.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance, RouteOptions } from 'fastify';
2 |
3 | import { Routes } from '@interfaces/routes.interface';
4 |
5 | import AuthController from '@components/auth/auth.controller';
6 | import UserController from '@components/user/user.controller';
7 |
8 | import { CreateUserSchema } from '@components/user/user.schema';
9 | import { LoginUserSchema } from '@components/auth/auth.schema';
10 |
11 | class AuthRoute implements Routes {
12 | public path = '/auth';
13 |
14 | public userController = new UserController();
15 |
16 | public authController = new AuthController();
17 |
18 | public initializeRoutes(fastify: FastifyInstance, opts: RouteOptions, done: () => void) {
19 | fastify.route({
20 | method: 'post',
21 | url: `${this.path}/signup`,
22 | schema: CreateUserSchema,
23 | handler: this.userController.createUser
24 | });
25 |
26 | fastify.route({
27 | method: 'post',
28 | url: `${this.path}/login`,
29 | schema: LoginUserSchema,
30 | handler: this.authController.login
31 | });
32 | done();
33 | }
34 | }
35 |
36 | export default AuthRoute;
37 |
--------------------------------------------------------------------------------
/src/components/auth/auth.schema.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@fastify/type-provider-typebox';
2 | import { FastifySchema } from 'fastify';
3 | import { ERROR400, ERROR401, ERROR404, ERROR409, ERROR500, responseProperty } from '@constants/constants';
4 |
5 | export const LoginUserBody = Type.Object({
6 | email: Type.String({ format: 'email', errorMessage: { format: 'Invalid Email' } }),
7 | password: Type.String()
8 | });
9 |
10 | export const LoginUserSchema: FastifySchema = {
11 | description: 'Login api',
12 | tags: ['auth'],
13 | body: LoginUserBody,
14 | response: {
15 | 201: {
16 | description: 'Successful login response',
17 | type: 'object',
18 | properties: {
19 | ...responseProperty,
20 | data: { type: 'object', properties: { accessToken: { type: 'string' } } }
21 | }
22 | },
23 | 400: ERROR400,
24 | 401: ERROR401,
25 | 404: ERROR404,
26 | 409: ERROR409,
27 | 500: ERROR500
28 | }
29 | };
30 |
--------------------------------------------------------------------------------
/src/components/auth/auth.service.ts:
--------------------------------------------------------------------------------
1 | import { compare } from 'bcrypt';
2 |
3 | import { LoginUser } from '@components/auth/auth.interface';
4 |
5 | import prisma from '@utils/prisma';
6 |
7 | import { NotFound, Unauthorized } from '@exceptions/error';
8 | import { FastifyReply } from 'fastify';
9 |
10 | class AuthService {
11 | public db = prisma;
12 |
13 | public async LoginUser(loginData: LoginUser, reply: FastifyReply) {
14 | const findUser = await this.db.user.findUnique({
15 | where: {
16 | email: loginData.email
17 | }
18 | });
19 |
20 | if (!findUser) {
21 | throw new NotFound('User not found');
22 | }
23 |
24 | // compare hashed and password
25 | const isPasswordMatching: boolean = await compare(loginData.password, findUser.password).catch(() => false);
26 |
27 | if (!isPasswordMatching) {
28 | throw new Unauthorized('Incorrect login credentials');
29 | }
30 |
31 | // example jwt
32 | const accessToken = await reply.jwtSign(
33 | {
34 | id: findUser.id,
35 | email: findUser.email
36 | },
37 | { sign: { expiresIn: '15m' } }
38 | );
39 |
40 | return { accessToken };
41 | }
42 | }
43 |
44 | export default AuthService;
45 |
--------------------------------------------------------------------------------
/src/components/default/index.controller.ts:
--------------------------------------------------------------------------------
1 | import { FastifyReply, FastifyRequest } from 'fastify';
2 |
3 | class IndexController {
4 | public static index = (req: FastifyRequest, reply: FastifyReply): void => {
5 | reply.send('ok');
6 | };
7 | }
8 |
9 | export default IndexController;
10 |
--------------------------------------------------------------------------------
/src/components/default/index.route.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance, RouteOptions } from 'fastify';
2 |
3 | import { Routes } from '@interfaces/routes.interface';
4 |
5 | import IndexController from '@components/default/index.controller';
6 |
7 | class IndexRoute implements Routes {
8 | public path = '/';
9 |
10 | public indexController = new IndexController();
11 |
12 | public initializeRoutes(fastify: FastifyInstance, opts: RouteOptions, done: () => void) {
13 | fastify.route({
14 | method: 'GET',
15 | url: this.path,
16 | schema: {
17 | response: {
18 | 200: {
19 | description: 'Successful response',
20 | type: 'string',
21 | example: 'ok'
22 | }
23 | }
24 | },
25 | handler: IndexController.index
26 | });
27 | done();
28 | }
29 | }
30 |
31 | export default IndexRoute;
32 |
--------------------------------------------------------------------------------
/src/components/user/user.controller.ts:
--------------------------------------------------------------------------------
1 | import { FastifyRequest } from 'fastify';
2 |
3 | import { CreateUser, GetUser } from '@components/user/user.interface';
4 |
5 | import UserService from '@components/user/user.service';
6 |
7 | class UserController {
8 | public userService = new UserService();
9 |
10 | public createUser = async (req: FastifyRequest<{ Body: CreateUser }>) => {
11 | const { email, password } = req.body;
12 |
13 | const data = await this.userService.createUser({ email, password });
14 |
15 | return { data, message: 'user created' };
16 | };
17 |
18 | public getUser = async (req: FastifyRequest) => {
19 | const { email } = req.user as GetUser;
20 |
21 | const data = await this.userService.getUser({ email });
22 |
23 | return { data, message: 'get user' };
24 | };
25 | }
26 |
27 | export default UserController;
28 |
--------------------------------------------------------------------------------
/src/components/user/user.interface.ts:
--------------------------------------------------------------------------------
1 | import { Static } from '@fastify/type-provider-typebox';
2 |
3 | import { CreateUserBody } from '@components/user/user.schema';
4 |
5 | export type CreateUser = Static;
6 |
7 | export interface GetUser {
8 | email: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/components/user/user.route.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@interfaces/routes.interface';
2 | import { FastifyInstance, RouteOptions } from 'fastify';
3 | import UserController from '@components/user/user.controller';
4 |
5 | import { GetUserSchema } from '@components/user/user.schema';
6 |
7 | class UserRoute implements Routes {
8 | public path = '/user';
9 |
10 | public userController = new UserController();
11 |
12 | public initializeRoutes(fastify: FastifyInstance, opts: RouteOptions, done: () => void) {
13 | fastify.route({
14 | method: 'get',
15 | url: this.path,
16 | schema: GetUserSchema,
17 | preHandler: fastify.authenticateUser,
18 | handler: this.userController.getUser
19 | });
20 | done();
21 | }
22 | }
23 |
24 | export default UserRoute;
25 |
--------------------------------------------------------------------------------
/src/components/user/user.schema.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@fastify/type-provider-typebox';
2 | import { FastifySchema } from 'fastify';
3 | import { ERROR400, ERROR401, ERROR404, ERROR409, ERROR500, responseProperty } from '@constants/constants';
4 |
5 | export const CreateUserBody = Type.Object({
6 | email: Type.String({ format: 'email', errorMessage: { format: 'Invalid Email' } }),
7 | password: Type.String({
8 | format: 'regex',
9 | pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[#?!@$%^&*-])(?=.{8,})',
10 | errorMessage: {
11 | pattern: 'password must minimum of 8 characters, 1 uppercase, lowercase, number and a special character'
12 | }
13 | })
14 | });
15 |
16 | export const CreateUserSchema: FastifySchema = {
17 | description: 'Create user api',
18 | tags: ['user'],
19 | body: CreateUserBody,
20 | response: {
21 | 201: {
22 | description: 'Successful create response',
23 | type: 'object',
24 | properties: {
25 | ...responseProperty,
26 | data: { type: 'object', properties: { email: { type: 'string' } } }
27 | }
28 | },
29 | 400: ERROR400,
30 | 409: ERROR409,
31 | 500: ERROR500
32 | }
33 | };
34 |
35 | //-------------------------------------------------------------------------------------------------------------------------
36 |
37 | export const GetUserSchema: FastifySchema = {
38 | description: 'Get user api',
39 | tags: ['user'],
40 | response: {
41 | 200: {
42 | description: 'Successful get response',
43 | type: 'object',
44 | properties: {
45 | ...responseProperty,
46 | data: { type: 'object', properties: { email: { type: 'string' } } }
47 | }
48 | },
49 | 400: ERROR400,
50 | 401: ERROR401,
51 | 404: ERROR404,
52 | 500: ERROR500
53 | }
54 | };
55 |
--------------------------------------------------------------------------------
/src/components/user/user.service.ts:
--------------------------------------------------------------------------------
1 | import { hash } from 'bcrypt';
2 |
3 | import { CreateUser, GetUser } from '@components/user/user.interface';
4 |
5 | import prisma from '@utils/prisma';
6 |
7 | import { Conflict, NotFound } from '@exceptions/error';
8 |
9 | class UserService {
10 | public db = prisma;
11 |
12 | private saltRounds = 10;
13 |
14 | public async createUser(createData: CreateUser) {
15 | const checkUserExists = await this.db.user.findUnique({
16 | where: {
17 | email: createData.email
18 | }
19 | });
20 |
21 | if (checkUserExists) {
22 | throw new Conflict('User already exists');
23 | }
24 |
25 | const hashedPassword = await hash(createData.password, this.saltRounds);
26 |
27 | const user = await this.db.user.create({
28 | data: {
29 | email: createData.email,
30 | password: hashedPassword
31 | },
32 | select: {
33 | email: true
34 | }
35 | });
36 |
37 | return user;
38 | }
39 |
40 | public async getUser(getUserData: GetUser) {
41 | const findUser = await this.db.user.findUnique({
42 | where: {
43 | email: getUserData.email
44 | }
45 | });
46 |
47 | if (!findUser) {
48 | throw new NotFound('User not found');
49 | }
50 |
51 | return findUser;
52 | }
53 | }
54 |
55 | export default UserService;
56 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 |
3 | config({ path: `.env.${process.env.NODE_ENV ?? 'development'}.local` });
4 |
5 | export const { NODE_ENV, PORT, API_VERSION, ORIGIN, CREDENTIALS, SECRET_KEY } = process.env;
6 |
--------------------------------------------------------------------------------
/src/constants/constants.ts:
--------------------------------------------------------------------------------
1 | export const responseProperty = {
2 | message: {
3 | type: 'string'
4 | }
5 | };
6 |
7 | export const ERROR400 = {
8 | description: 'Bad request',
9 | type: 'object',
10 | properties: responseProperty
11 | };
12 |
13 | export const ERROR401 = {
14 | description: 'Unauthorized',
15 | type: 'object',
16 | properties: responseProperty
17 | };
18 |
19 | export const ERROR403 = {
20 | description: 'Forbidden Request',
21 | properties: responseProperty
22 | };
23 |
24 | export const ERROR404 = {
25 | description: 'Not found',
26 | properties: responseProperty
27 | };
28 |
29 | export const ERROR409 = {
30 | description: 'Conflict',
31 | properties: responseProperty
32 | };
33 |
34 | export const ERROR500 = {
35 | description: 'Internal Sever Error',
36 | properties: responseProperty
37 | };
38 |
--------------------------------------------------------------------------------
/src/exceptions/error.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line max-classes-per-file
2 | export class NotFound extends Error {
3 | statusCode: number;
4 |
5 | constructor(message = 'Not found') {
6 | super(message);
7 | this.statusCode = 404;
8 | }
9 | }
10 |
11 | export class Conflict extends Error {
12 | statusCode: number;
13 |
14 | constructor(message = 'Conflict') {
15 | super(message);
16 | this.statusCode = 409;
17 | }
18 | }
19 |
20 | export class Unauthorized extends Error {
21 | statusCode: number;
22 |
23 | constructor(message = 'Unauthorized') {
24 | super(message);
25 | this.statusCode = 401;
26 | }
27 | }
28 |
29 | export class BadRequest extends Error {
30 | statusCode: number;
31 |
32 | constructor(message = 'Bad Request') {
33 | super(message);
34 | this.statusCode = 400;
35 | }
36 | }
37 |
38 | export class Forbidden extends Error {
39 | statusCode: number;
40 |
41 | constructor(message = 'Forbidden') {
42 | super(message);
43 | this.statusCode = 403;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/interfaces/routes.interface.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance, RouteOptions } from 'fastify';
2 |
3 | export interface Routes {
4 | path: string;
5 | initializeRoutes: (fastify: FastifyInstance, opts: RouteOptions, done: () => void) => void;
6 | }
7 |
--------------------------------------------------------------------------------
/src/plugins/authentication.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance, FastifyRequest } from 'fastify';
2 | import { fastifyPlugin } from 'fastify-plugin';
3 |
4 | import prisma from '@utils/prisma';
5 | import { Unauthorized } from '@exceptions/error';
6 |
7 | export const authentication = fastifyPlugin((fastify: FastifyInstance, _: unknown, done: () => void) => {
8 | const authPreHandler = async (request: FastifyRequest) => {
9 | try {
10 | const authorization =
11 | (request.headers.authorization ? request.headers.authorization?.split('Bearer ')[1] : '') || '';
12 |
13 | const payload = fastify.jwt.verify(authorization) as { email: string };
14 |
15 | const getUser = await prisma.user.findUnique({
16 | where: {
17 | email: payload.email
18 | }
19 | });
20 | if (!getUser) {
21 | throw Error();
22 | }
23 |
24 | request.user = getUser;
25 | } catch (error) {
26 | throw new Unauthorized();
27 | }
28 | };
29 | fastify.decorate('authenticateUser', authPreHandler);
30 | done();
31 | });
32 |
--------------------------------------------------------------------------------
/src/plugins/initializeRoute.ts:
--------------------------------------------------------------------------------
1 | import { FastifyPluginCallbackTypebox } from '@fastify/type-provider-typebox';
2 | import { FastifyPluginOptions } from 'fastify';
3 |
4 | import { Routes } from '@interfaces/routes.interface';
5 |
6 | import AuthRoute from '@components/auth/auth.route';
7 | import IndexRoute from '@components/default/index.route';
8 | import UserRoute from '@components/user/user.route';
9 |
10 | export const initializeRoutes: FastifyPluginCallbackTypebox = (server, options, done) => {
11 | // add the new routes here
12 | const routes = [new IndexRoute(), new UserRoute(), new AuthRoute()];
13 | routes.forEach((route: Routes) => {
14 | server.register(route.initializeRoutes.bind(route));
15 | });
16 | done();
17 | };
18 |
--------------------------------------------------------------------------------
/src/plugins/swagger.ts:
--------------------------------------------------------------------------------
1 | import { FastifyInstance } from 'fastify';
2 | import fastifySwagger, { FastifyDynamicSwaggerOptions } from '@fastify/swagger';
3 | import fastifySwaggerUi, { FastifySwaggerUiOptions } from '@fastify/swagger-ui';
4 | import { fastifyPlugin } from 'fastify-plugin';
5 |
6 | export const initSwagger = fastifyPlugin((fastify: FastifyInstance, _: unknown, done: () => void) => {
7 | const opts: FastifyDynamicSwaggerOptions = {
8 | swagger: {
9 | info: {
10 | title: 'Fastify Swagger',
11 | description: "swagger documentation for api's in the boilerplate",
12 | version: '1.0.0'
13 | },
14 | tags: [
15 | { name: 'user', description: 'User related end-points' },
16 | { name: 'auth', description: 'Authentication end-points' }
17 | ],
18 | consumes: ['application/json'],
19 | produces: ['application/json'],
20 | securityDefinitions: {
21 | bearerAuth: {
22 | type: 'apiKey',
23 | name: 'Authorization',
24 | in: 'header'
25 | }
26 | },
27 | schemes: ['http'],
28 | security: []
29 | }
30 | };
31 |
32 | fastify.register(fastifySwagger, opts);
33 |
34 | const uiOpts: FastifySwaggerUiOptions = {
35 | routePrefix: '/api-docs',
36 | staticCSP: true,
37 | transformStaticCSP: (header) => header,
38 | uiConfig: {
39 | docExpansion: 'list',
40 | deepLinking: false
41 | }
42 | };
43 |
44 | fastify.register(fastifySwaggerUi, uiOpts);
45 | done();
46 | });
47 |
--------------------------------------------------------------------------------
/src/prisma/migrations/20230718134925_init/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "User" (
3 | "id" SERIAL NOT NULL,
4 | "email" TEXT NOT NULL,
5 |
6 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
7 | );
8 |
9 | -- CreateIndex
10 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
11 |
--------------------------------------------------------------------------------
/src/prisma/migrations/20230721125416_added_password_to_user/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - Added the required column `password` to the `User` table without a default value. This is not possible if the table is not empty.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "User" ADD COLUMN "password" TEXT NOT NULL;
9 |
--------------------------------------------------------------------------------
/src/prisma/migrations/20230721135627_added_char_limit_to_password/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to alter the column `password` on the `User` table. The data in that column could be lost. The data in that column will be cast from `Text` to `VarChar(100)`.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "User" ALTER COLUMN "password" SET DATA TYPE VARCHAR(100);
9 |
--------------------------------------------------------------------------------
/src/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/src/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | datasource db {
2 | provider = "postgresql"
3 | url = env("DATABASE_URL")
4 | }
5 |
6 | generator client {
7 | provider = "prisma-client-js"
8 | }
9 |
10 | model User {
11 | id Int @id @default(autoincrement())
12 | email String @unique
13 | password String @db.VarChar(100)
14 | }
15 |
--------------------------------------------------------------------------------
/src/prisma/seed.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TheB1gFatPanda/fastify-typescript-starter/749bab34c1885ec06d8bc5bd01cf6df251d94e25/src/prisma/seed.ts
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import App from './app';
2 |
3 | const app = new App();
4 |
5 | app.listen();
6 |
--------------------------------------------------------------------------------
/src/types/fastify.d.ts:
--------------------------------------------------------------------------------
1 | import { FastifyReply, FastifyRequest } from 'fastify';
2 | import { Static } from '@fastify/type-provider-typebox';
3 | import { schema } from '@utils/validateEnv';
4 |
5 | declare module 'fastify' {
6 | interface FastifyInstance {
7 | authenticateUser?: (request: FastifyRequest, reply: FastifyReply) => Promise;
8 | }
9 | }
10 |
11 | declare module 'fastify' {
12 | interface FastifyInstance {
13 | config: Static;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { Static } from '@fastify/type-provider-typebox';
2 | import { schema } from '@utils/validateEnv';
3 |
4 | declare global {
5 | namespace NodeJS {
6 | interface ProcessEnv extends Static {}
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/prisma.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const prisma = new PrismaClient();
4 |
5 | export default prisma;
6 |
--------------------------------------------------------------------------------
/src/utils/schemaErrorFormatter.ts:
--------------------------------------------------------------------------------
1 | import { FastifySchemaValidationError } from 'fastify/types/schema';
2 |
3 | export const schemaErrorFormatter = (errors: FastifySchemaValidationError[]) => {
4 | if (errors.length === 0) {
5 | return new Error('Validation failed: No errors found.');
6 | }
7 |
8 | const firstError = errors[0];
9 | const instancePath = firstError?.instancePath.substring(1) ?? '';
10 | const message = firstError?.message ?? '';
11 | const formattedError = `${instancePath}${instancePath ? ': ' : ''}${message}`;
12 |
13 | return new Error(formattedError);
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/validateEnv.ts:
--------------------------------------------------------------------------------
1 | import { Type } from '@sinclair/typebox';
2 |
3 | export const schema = Type.Object({
4 | NODE_ENV: Type.String(),
5 | API_VERSION: Type.String(),
6 | ORIGIN: Type.String(),
7 | SECRET_KEY: Type.String(),
8 | PORT: Type.String()
9 | });
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "target": "ES2022",
5 | "lib": ["ES2022", "esnext.asynciterable", "DOM"],
6 | "typeRoots": ["node_modules/@types"],
7 | "allowSyntheticDefaultImports": true,
8 | "experimentalDecorators": true,
9 | "emitDecoratorMetadata": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noUncheckedIndexedAccess": true,
12 | "moduleResolution": "node",
13 | "module": "commonjs",
14 | "pretty": true,
15 | "sourceMap": true,
16 | "strict": true,
17 | "declaration": true,
18 | "outDir": "dist",
19 | "allowJs": true,
20 | "noEmit": false,
21 | "esModuleInterop": true,
22 | "resolveJsonModule": true,
23 | "importHelpers": true,
24 | "baseUrl": "src",
25 | "paths": {
26 | "@/*": ["*"],
27 | "@config": ["config"],
28 | "@interfaces/*": ["interfaces/*"],
29 | "@utils/*": ["utils/*"],
30 | "@constants/*": ["constants/*"],
31 | "@plugins/*": ["plugins/*"],
32 | "@components/*": ["components/*"],
33 | "@exceptions/*": ["exceptions/*"]
34 | }
35 | },
36 | "include": ["src/**/*.ts", "src/**/*.json", ".env", "src/**/*.feature", "src/**/*.d.ts"],
37 | "exclude": ["node_modules", "src/http", "src/logs", "src/tests"]
38 | }
39 |
--------------------------------------------------------------------------------