├── .github ├── ISSUE_TEMPLATE.md ├── ISSUE_TEMPLATE │ ├── example.md │ └── not-works.md └── PULL_REQUEST_TEMPLATE │ ├── hosted-project.md │ └── self-hosted-project.md ├── .gitignore ├── LICENSE ├── README.md ├── fastify-postgres ├── .postgratorrc.json ├── Dockerfile ├── README.md ├── docker-compose.yaml ├── index.js ├── migrations │ ├── 001.do.books.sql │ └── 001.undo.books.sql ├── package.json ├── server.js └── test.js ├── fastify-session-authentication ├── README.md ├── authentication.js ├── authentication.test.js ├── html.js └── package.json ├── tests ├── app.js ├── database-connection │ ├── mongodb-setup.js │ └── mongodb.test.js ├── package.json └── server.js ├── typescript-decorators ├── .gitignore ├── README.md ├── bin │ └── app.js ├── package.json ├── src │ ├── controllers │ │ └── main.controller.ts │ ├── services │ │ └── message.service.ts │ └── typescript-decorators.ts ├── test │ └── main.controller.spec.ts └── tsconfig.json ├── validation-messages ├── README.md ├── custom-errors-messages.js ├── custom-errors-messages.test.js └── package.json └── winston-logger ├── README.md ├── package.json └── winston-logger.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## 👉 [Please follow one of these issue templates](https://github.com/fastify/example/issues/new/choose) 👈 2 | 3 | #### Are you just looking for some help? 4 | 5 | For all other questions, requests, help resolving an issue, or if you are not sure if this is 6 | the right place, please do not open an issue here. Instead, ask a question in our [help](https://github.com/fastify/help) repository. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📐 Example Proposal 3 | about: Submit a proposal for a new example 4 | --- 5 | 6 | ## 📐 Example Proposal 7 | 8 | 11 | 12 | ## Motivation 13 | 14 | 18 | 19 | ## Code 20 | 21 | 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/not-works.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ⚔ Example does not work 3 | about: Submit an issue for not working examples 4 | --- 5 | 6 | ## ⚔ Example does not work 7 | 8 | 11 | 12 | ## Reproduce 13 | 14 | 17 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/hosted-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ New example project 3 | about: Add an example project to this repository 4 | --- 5 | 6 | 12 | 13 | #### Checklist for a new example 14 | 15 | - [ ] the project has a dedicated folder in `kebab-case` 16 | - [ ] there is not an `index.js`, but a valuable name (usaully the same of the folder) 17 | - [ ] a test example is included (preferring using `tap`) 18 | - [ ] the internal `README.md` file describe the project itself 19 | - [ ] there are comments on code that explain the most difficult parts 20 | - [ ] the project has been added in the examples list in the root `README.md` of this repository 21 | - [ ] the `package-lock.json` is commited 22 | - [ ] commit message and code follows Fastify's [Code of conduct](https://github.com/fastify/fastify/blob/main/CODE_OF_CONDUCT.md) 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/self-hosted-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🎈 External example 3 | about: Add an external project to the example list 4 | --- 5 | 12 | 13 | #### Checklist for a new example 14 | 15 | - [ ] the project is for training 16 | - [ ] a test example is included 17 | - [ ] the internal `README.md` file describe the project itself 18 | - [ ] there are comments on code that explain the most difficult parts 19 | - [ ] the project has been added in the external example list in the root `README.md` of this repository 20 | - [ ] a CI tool is in place to verify the tests 21 | - [ ] commit message and code follows Fastify's [Code of conduct](https://github.com/fastify/fastify/blob/main/CODE_OF_CONDUCT.md) 22 | -------------------------------------------------------------------------------- /.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 | # Vim swap files 133 | *.swp 134 | 135 | # macOS files 136 | .DS_Store 137 | 138 | # lock files 139 | package-lock.json 140 | pnpm-lock.yaml 141 | yarn.lock 142 | 143 | # editor files 144 | .vscode 145 | .idea 146 | 147 | #tap files 148 | .tap/ 149 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fastify 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 | # example 2 | 3 | This repository is a collection of Fastify project that can be used to understand `how-to` archive commons 4 | problems and requirements. 5 | 6 | Every directory is a stand alone project that you can read to undestand how it works and how it is configured. 7 | 8 | ## Projects 9 | 10 | Here a list of the projects with a description, search in this page what you are looking for 11 | 12 | | Project | Tags | Description | 13 | |---------|------|-------------| 14 | | [authentication] | `authentication` `session` | Example how to do authentication with `fastify-session` | 15 | | [validation-messages] | `schema` `validation` | How you can customize the error messages of input schema validation | 16 | | [winston-logger] | `logger` | Example how to use winston as a custom logger | 17 | | [typescript decorators] | `typescript` | Example how to use typescript decorators to build application | 18 | | [fastify postgres] | `postgres` `crud` | Simple CRUD app that show how integrate fastify with database, with 100% test coverage | 19 | | [tests] | `tests` | Example of how to test your fastify application | 20 | 21 | 22 | ## External Projects 23 | 24 | Here a list of external projects that could be useful to find some tips and suggestions 25 | 26 | | Project | Tags | Description | 27 | |---------|------|-------------| 28 | 29 | 30 | ## Contributions 31 | 32 | PR are welcome! Consider that a project to be added to this repository needs to have: 33 | 34 | + A `README.md` file that exaplain the project itself 35 | + consider to use some [mermaid graph](https://mermaidjs.github.io) 36 | + Prefer a readable code, instead of concise 37 | + Comments on code that explain some code if there are more difficult parts 38 | + At least one test that show how to test the main function of the project 39 | 40 | ## License 41 | 42 | Licensed under [MIT](./LICENSE). 43 | 44 | [authentication]: ./fastify-session-authentication 45 | [validation-messages]:./validation-messages/ 46 | [winston-logger]: ./winston-logger 47 | [typescript decorators]: ./typescript-decorators 48 | [fastify postgres]: ./fastify-postgres 49 | -------------------------------------------------------------------------------- /fastify-postgres/.postgratorrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrationPattern": "migrations/*", 3 | "driver": "pg", 4 | "host": "localhost", 5 | "port": 5432, 6 | "database": "fastify_postgres", 7 | "username": "postgres", 8 | "password": "postgres" 9 | } 10 | -------------------------------------------------------------------------------- /fastify-postgres/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14-alpine 2 | 3 | WORKDIR /src 4 | COPY package.json / 5 | EXPOSE 3000 6 | 7 | # on CI normaly you use npm ci / npm clean-install 8 | RUN npm install 9 | COPY . / 10 | CMD ["npm", "start"] 11 | 12 | 13 | -------------------------------------------------------------------------------- /fastify-postgres/README.md: -------------------------------------------------------------------------------- 1 | # Fastify Postgres CRUD 2 | 3 | Motivation, this simple app are meant to show on how to use Postgres alongside with Fastify, and to show how to achieve 100% test coverage with it. 4 | 5 | ## Requirements 6 | 7 | - Docker & Docker Compose 8 | - Node.js 9 | 10 | ## How to run? 11 | 12 | With npm 13 | 14 | ``` 15 | $ npm start:db // start postgres db with docker 16 | $ npm install 17 | $ npm start 18 | 19 | # testing 20 | $ npm test 21 | ``` 22 | 23 | With docker compose 24 | 25 | ``` 26 | # migration up 27 | $ docker-compose -f docker-compose.yaml -f compose-file/migration-up.yaml --exit-code-from fastify_postgres 28 | 29 | # run the app 30 | $ docker-compose up 31 | 32 | # testing 33 | $ docker-compose -f docker-compose.yaml -f compose-file/test.yaml --exit-code-from fastify_postgres 34 | 35 | # migration down 36 | $ docker-compose -f docker-compose.yaml -f compose-file/migration-up.yaml --exit-code-from fastify_postgres 37 | 38 | ``` 39 | -------------------------------------------------------------------------------- /fastify-postgres/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | db: 4 | image: postgres:latest 5 | expose: 6 | - "5432" 7 | ports: 8 | - "5432:5432" 9 | environment: 10 | - POSTGRES_DB=fastify_postgres 11 | - POSTGRES_USER=postgres 12 | - POSTGRES_PASSWORD=postgres 13 | 14 | fastify_postgres: 15 | build: 16 | context: ./ 17 | dockerfile: Dockerfile 18 | volumes: 19 | - .:/src 20 | command: > 21 | sh -c "npm start" 22 | expose: 23 | - "3000" 24 | ports: 25 | - "3000:3000" 26 | depends_on: 27 | - db 28 | environment: 29 | - DATABASE_URL=postgresql://postgres:postgres@db_postgres:5432/fastify_postgres?schema=public 30 | -------------------------------------------------------------------------------- /fastify-postgres/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const app = require("./server")({ logger: true }); 4 | 5 | const start = async () => { 6 | try { 7 | await app.listen(3000); 8 | } catch (err) { 9 | app.log.error(err); 10 | process.exit(1); 11 | } 12 | }; 13 | 14 | start(); 15 | -------------------------------------------------------------------------------- /fastify-postgres/migrations/001.do.books.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS books ( 2 | id serial PRIMARY KEY, 3 | title varchar (255) NOT NULL 4 | ) 5 | -------------------------------------------------------------------------------- /fastify-postgres/migrations/001.undo.books.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS books 2 | -------------------------------------------------------------------------------- /fastify-postgres/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-database-example", 3 | "version": "1.0.0", 4 | "description": "Example on how to use, fastify with postgres database unit and integration test", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node --test", 8 | "start": "node index.js", 9 | "start:db": "docker-compose up -d db", 10 | "migrate:up": "npx postgrator", 11 | "migrate:down": "npx postgrator 0" 12 | }, 13 | "keywords": [ 14 | "postgres", 15 | "fastify" 16 | ], 17 | "author": "Manda Putra (https://github.com/mandaputtra)", 18 | "license": "MIT", 19 | "dependencies": { 20 | "fastify": "^5.2.1", 21 | "fastify-postgres": "^3.7.0", 22 | "pg": "^8.13.1" 23 | }, 24 | "devDependencies": { 25 | "postgrator-cli": "^9.0.1" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /fastify-postgres/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fastify = require("fastify"); 4 | 5 | const connectionString = 6 | process.env.DATABASE_URL || 7 | "postgresql://postgres:postgres@localhost:5432/fastify_postgres?schema=public"; 8 | 9 | function build(opts = {}) { 10 | const app = fastify(opts); 11 | 12 | app.register(require("fastify-postgres"), { connectionString }); 13 | 14 | app.get("/", async () => { 15 | const client = await app.pg.connect(); 16 | const { rows } = await client.query("SELECT * FROM books"); 17 | client.release(); 18 | return rows; 19 | }); 20 | 21 | app.post("/", async (request) => { 22 | const { body } = request; 23 | const client = await app.pg.connect(); 24 | const { rows } = await client.query( 25 | "INSERT INTO books (title) VALUES ($1) RETURNING *", 26 | [body.title] 27 | ); 28 | client.release(); 29 | return rows; 30 | }); 31 | 32 | app.get("/:id", async (request) => { 33 | const { params } = request; 34 | const client = await app.pg.connect(); 35 | const { rows } = await client.query("SELECT * FROM books WHERE id = $1", [ 36 | +params.id, 37 | ]); 38 | client.release(); 39 | return rows; 40 | }); 41 | 42 | app.patch("/:id", async (request) => { 43 | const { params, body } = request; 44 | return app.pg.transact(async (client) => { 45 | await client.query("UPDATE books SET title = $1 WHERE id = $2", [ 46 | body.title, 47 | +params.id, 48 | ]); 49 | const { rows } = await client.query("SELECT * FROM books WHERE id = $1", [ 50 | +params.id, 51 | ]); 52 | return rows; 53 | }); 54 | }); 55 | 56 | app.delete("/:id", async (request) => { 57 | const { params } = request; 58 | const client = await app.pg.connect(); 59 | const { rowCount } = await client.query("DELETE FROM books WHERE id = $1", [ 60 | +params.id, 61 | ]); 62 | client.release(); 63 | return { rowCount }; 64 | }); 65 | 66 | return app; 67 | } 68 | 69 | module.exports = build; 70 | -------------------------------------------------------------------------------- /fastify-postgres/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const {test} = require("node:test"); 4 | const fastify = require("./server"); 5 | 6 | test("should test all API endpoint", async (t) => { 7 | let book = {}; 8 | 9 | const app = await fastify(); 10 | 11 | t.after(() => app.close()); 12 | // only to test error path 13 | await t.test("POST / should success create item", async (t) => { 14 | const response = await app.inject({ 15 | method: "POST", 16 | url: "/", 17 | payload: { 18 | title: "Hello, World!", 19 | }, 20 | }); 21 | const json = response.json(); 22 | t.assert.deepStrictEqual(response.statusCode, 200); 23 | t.assert.deepStrictEqual(json[0].title, "Hello, World!"); 24 | // assign created to use as compare value 25 | book = json[0]; 26 | }); 27 | 28 | await t.test("GET / should success return items", async (t) => { 29 | const response = await app.inject({ 30 | method: "GET", 31 | url: "/", 32 | }); 33 | const json = response.json(); 34 | t.assert.deepStrictEqual(response.statusCode, 200); 35 | t.assert.deepStrictEqual(json.length > 0, true); 36 | t.assert.deepStrictEqual(json[0].title, "Hello, World!"); 37 | }); 38 | 39 | await t.test("GET /:id should success return item", async (t) => { 40 | const response = await app.inject({ 41 | method: "GET", 42 | url: `/${book.id}`, 43 | }); 44 | const json = response.json(); 45 | t.assert.deepStrictEqual(response.statusCode, 200); 46 | t.assert.deepStrictEqual(json[0].title, "Hello, World!"); 47 | }); 48 | 49 | await t.test("UPDATE /:id should success return item", async (t) => { 50 | const response = await app.inject({ 51 | method: "PATCH", 52 | url: `/${book.id}`, 53 | payload: { 54 | title: "Hello again, World!", 55 | }, 56 | }); 57 | const json = response.json(); 58 | t.assert.deepStrictEqual(response.statusCode, 200); 59 | t.assert.deepStrictEqual(json[0].title, "Hello again, World!"); 60 | }); 61 | 62 | await t.test("DELETE /:id should success delete item", async (t) => { 63 | const response = await app.inject({ 64 | method: "DELETE", 65 | url: `/${book.id}`, 66 | }); 67 | const json = response.json(); 68 | t.assert.deepStrictEqual(response.statusCode, 200); 69 | t.assert.deepStrictEqual(json.rowCount, 1); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /fastify-session-authentication/README.md: -------------------------------------------------------------------------------- 1 | # fastify-session authentication example 2 | 3 | This example shows how to do authentication with [fastify-session](https://github.com/fastify/session). 4 | 5 | This project can simply run by: 6 | 7 | ```sh 8 | cd fastify-session-authentication 9 | npm install 10 | npm start 11 | ``` 12 | 13 | Then open `http://localhost:3000/login` in your browser and use the `abcdef` password to login. 14 | -------------------------------------------------------------------------------- /fastify-session-authentication/authentication.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastifySession = require('@fastify/session') 4 | const fastifyCookie = require('@fastify/cookie') 5 | const fastifyFormbody = require('@fastify/formbody') 6 | 7 | const { loginPage, defaultPage } = require('./html') 8 | 9 | function plugin (instance, options, next) { 10 | // register the required plugins 11 | instance.register(fastifyFormbody) 12 | instance.register(fastifyCookie) 13 | instance.register(fastifySession, { 14 | cookieName: 'sessionId', 15 | secret: 'a secret with minimum length of 32 characters', 16 | cookie: { maxAge: 1800000, secure: false } 17 | }) 18 | 19 | // add a login route that returns a login page 20 | instance.get('/login', (request, reply) => { 21 | reply.type('text/html') 22 | reply.send(loginPage()) 23 | }) 24 | 25 | // add a login route that handles the actual login 26 | instance.post('/login', (request, reply) => { 27 | const { email, password } = request.body 28 | 29 | if (password === 'abcdef') { 30 | request.session.authenticated = true 31 | reply.type('text/html') 32 | reply.send(defaultPage(true)) 33 | } else { 34 | reply.redirect('/login', 401) 35 | } 36 | }); 37 | 38 | instance.get('/', (request, reply) => { 39 | reply.type('text/html') 40 | reply.send(defaultPage(request.session.authenticated)) 41 | }); 42 | 43 | // add a logout route 44 | instance.get('/logout', (request, reply) => { 45 | if (request.session.authenticated) { 46 | request.session.destroy((err) => { 47 | if (err) { 48 | reply.status(500) 49 | reply.send('Internal Server Error') 50 | } else { 51 | reply.redirect('/') 52 | } 53 | }) 54 | } else { 55 | reply.redirect('/') 56 | } 57 | }); 58 | 59 | next() 60 | } 61 | 62 | module.exports = plugin 63 | -------------------------------------------------------------------------------- /fastify-session-authentication/authentication.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const plugin = require('./authentication') 5 | const axios = require('axios') 6 | const fastify = require('fastify') 7 | 8 | test('should be able to login', async (t) => { 9 | t.plan(3) 10 | 11 | const app = await server() 12 | const { port } = app.server.address() 13 | 14 | const result0 = await performLogin(port, 'abcdef') 15 | const result1 = await toHomePage(port, result0.sessionCookie) 16 | 17 | t.assert.ok(result0.body.includes('logged in')) 18 | t.assert.ok(result1.body.includes('logged in')) 19 | t.assert.equal(result1.sessionCookie, result0.sessionCookie) 20 | app.close() 21 | }) 22 | 23 | test('should not be able to login with wrong password', async (t) => { 24 | t.plan(2) 25 | 26 | const app = await server() 27 | const { port } = app.server.address() 28 | 29 | const { location, statusCode } = await performLogin(port, '123456') 30 | 31 | t.assert.equal(location, '/login') 32 | t.assert.equal(statusCode, 401) 33 | app.close() 34 | }) 35 | 36 | test('should be not logged in', async (t) => { 37 | t.plan(2) 38 | 39 | const app = await server() 40 | const { port } = app.server.address() 41 | 42 | // login route 43 | const { sessionCookie, body } = await requestPath(port) 44 | 45 | t.assert.ok(body.includes('please login')) 46 | t.assert.ok(sessionCookie.includes('sessionId')) 47 | app.close() 48 | }) 49 | 50 | test('should be able to logout', async (t) => { 51 | t.plan(4) 52 | 53 | const app = await server() 54 | const { port } = app.server.address() 55 | 56 | const result0 = await performLogin(port, 'abcdef') 57 | const result1 = await toHomePage(port, result0.sessionCookie) 58 | const result2 = await logout(port, result0.sessionCookie) 59 | const result3 = await toHomePage(port, result2.sessionCookie) 60 | 61 | t.assert.ok(result1.body.includes('logged in')) 62 | t.assert.ok(result3.body.includes('please login')) 63 | t.assert.ok(result3.sessionCookie === result2.sessionCookie) 64 | t.assert.ok(result0.sessionCookie !== result2.sessionCookie) 65 | app.close() 66 | }) 67 | 68 | test('should be able to call logout if not logged in', async (t) => { 69 | t.plan(2) 70 | 71 | const app = await server() 72 | const { port } = app.server.address() 73 | 74 | // logout route 75 | const result0 = await requestPath(port, 'logout') 76 | const result1 = await toHomePage(port, result0.sessionCookie) 77 | 78 | t.assert.ok(result1.body.includes('please login')) 79 | t.assert.ok(result1.sessionCookie === result0.sessionCookie) 80 | app.close() 81 | }) 82 | 83 | test('should be able to request login page', async (t) => { 84 | t.plan(1) 85 | 86 | const app = await server() 87 | const { port } = app.server.address() 88 | 89 | const { statusCode } = await toLoginPage(port) 90 | 91 | t.assert.equal(statusCode, 200) 92 | app.close() 93 | }) 94 | 95 | async function server() { 96 | const app = fastify() 97 | app.register(plugin) 98 | app.server.unref() 99 | await app.listen() 100 | return app 101 | } 102 | 103 | async function toHomePage(port, cookie) { 104 | return request({ 105 | url: `http://localhost:${port}/`, 106 | method: 'GET', 107 | headers: { cookie } 108 | }); 109 | } 110 | 111 | async function performLogin(port, password) { 112 | return request({ 113 | url: `http://localhost:${port}/login`, 114 | method: 'POST', 115 | data: { email: 'test@test.de', password }, 116 | }); 117 | } 118 | 119 | async function toLoginPage(port) { 120 | return request({ 121 | url: `http://localhost:${port}/login`, 122 | method: 'GET' 123 | }); 124 | } 125 | 126 | async function logout(port, cookie) { 127 | return request({ 128 | url: `http://localhost:${port}/logout`, 129 | method: 'GET', 130 | headers: { cookie } 131 | }); 132 | } 133 | 134 | async function requestPath(port, path = '') { 135 | return request({ 136 | url: `http://localhost:${port}/${path}`, 137 | method: 'GET' 138 | }); 139 | } 140 | 141 | async function request (options) { 142 | try { 143 | const res = await axios.request(options) 144 | 145 | return { 146 | statusCode: res.status, 147 | body: res.data, 148 | sessionCookie: res.headers['set-cookie'][0], 149 | location: res.request.res.responseUrl 150 | } 151 | } catch (err) { 152 | return { 153 | body: err.response?.data, 154 | statusCode: err.response?.status, 155 | location: err.response?.headers?.location 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /fastify-session-authentication/html.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function loginPage () { 4 | return '' + 5 | 'Login' + 6 | '' + 7 | '

Login

' + 8 | '
' + 9 | '

Email

' + 10 | '' + 11 | '

Password

' + 12 | '' + 13 | '

' + 14 | '
' + 15 | '' + 16 | '' 17 | } 18 | 19 | function defaultPage (isAuthenticated) { 20 | if (isAuthenticated) { 21 | return 'logged in

Logout' 22 | } else { 23 | return 'please login

Login' 24 | } 25 | } 26 | 27 | module.exports = { 28 | loginPage, 29 | defaultPage 30 | } -------------------------------------------------------------------------------- /fastify-session-authentication/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "authentication", 3 | "version": "1.0.0", 4 | "description": "example of authentication using fastify-session", 5 | "main": "authentication.js", 6 | "scripts": { 7 | "start": "fastify start authentication.js -l debug -P", 8 | "test": "node --test" 9 | }, 10 | "author": "Denis Fäcke", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@fastify/cookie": "^11.0.2", 14 | "@fastify/formbody": "^8.0.2", 15 | "@fastify/session": "^11.1.0", 16 | "fastify": "^5.2.1" 17 | }, 18 | "devDependencies": { 19 | "axios": "^1.7.9", 20 | "fastify-cli": "^7.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/app.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastifyMongodb = require('fastify-mongodb') 4 | 5 | function app(fastify, config, done) { 6 | fastify.register(fastifyMongodb, { url: config.mongodb.url }) 7 | fastify.get('/', async function (_req, reply) { 8 | const db = this.mongo.client.db() 9 | const results = await db.collection('examples').find() 10 | return reply.send(results) 11 | }) 12 | 13 | done() 14 | } 15 | 16 | module.exports = app 17 | -------------------------------------------------------------------------------- /tests/database-connection/mongodb-setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const fp = require('fastify-plugin') 5 | 6 | const clean = require('mongo-clean') 7 | 8 | const App = require('../app') 9 | 10 | const url = 'mongodb://localhost/prod' 11 | const database = 'tests' 12 | 13 | // Fill in this config with all the configurations 14 | // needed for testing the application 15 | function config () { 16 | return { 17 | mongodb: { 18 | url, 19 | } 20 | } 21 | } 22 | 23 | // automatically build and tear down our instance 24 | function build (t) { 25 | const app = Fastify() 26 | app.register(fp(App), config()) 27 | 28 | t.after(async () => { 29 | await clean(app.mongo.client.db(database)) 30 | await app.close() 31 | }) 32 | return app 33 | } 34 | 35 | module.exports = { 36 | build 37 | } 38 | -------------------------------------------------------------------------------- /tests/database-connection/mongodb.test.js: -------------------------------------------------------------------------------- 1 | const { test } = require('node:test') 2 | const { build } = require('./mongodb-setup') 3 | 4 | test('GET / • returns 200', async (t) => { 5 | t.plan(2); 6 | const fastify = build(t); 7 | 8 | const response = await fastify.inject( 9 | { 10 | method: 'GET', 11 | url: '/', 12 | }); 13 | 14 | t.assert.strictEqual(response.statusCode, 200); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "database-connection", 3 | "version": "1.0.0", 4 | "description": "example of tests using database", 5 | "main": "mongodb-test.js", 6 | "scripts": { 7 | "test:database": "node --test" 8 | }, 9 | "author": "Fastify", 10 | "license": "MIT", 11 | "dependencies": { 12 | "fastify": "^5.2.1", 13 | "fastify-mongodb": "^4.2.0", 14 | "fastify-plugin": "^5.0.1" 15 | }, 16 | "devDependencies": { 17 | "mongo-clean": "^2.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/server.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const fp = require('fastify-plugin') 5 | const app = require('./app') 6 | 7 | async function server() { 8 | const fastify = Fastify({ logger: true }) 9 | const database = process.env.MONGO_URI || 'mongodb://localhost/prod' 10 | fastify.register( 11 | fp(app), 12 | { 13 | mongodb: { 14 | url: database 15 | } 16 | } 17 | ) 18 | 19 | fastify.listen(3000) 20 | } 21 | 22 | server() 23 | -------------------------------------------------------------------------------- /typescript-decorators/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /typescript-decorators/README.md: -------------------------------------------------------------------------------- 1 | # Typescript decorators 2 | 3 | Example how to use typescript decorators with Fastify to build controllers with handlers and hooks 4 | 5 | This example uses Fastify `^2.0.0` framework and fastify-decorators `^2.0.0-0`. In other versions typings could be different. 6 | 7 | Check now the [source code directory](src)! You will find all the info there in the comments. 8 | 9 | You can run this project simply by: 10 | 11 | ```sh 12 | cd typescript-decorators 13 | npm i 14 | npm run build 15 | npm start 16 | ``` 17 | 18 | ## How it works 19 | 1. Each controller (for example [main.controller.ts]) has `Controller` decorators 20 | 21 | 1. Each controller's handler has `GET`, `POST` or other method decorators 22 | 23 | 1. Controllers may have constructors with dependencies 24 | 25 | 1. Each dependency has `Service` decorator to be available for DI 26 | 27 | *Note*: dependencies also may have own dependencies 28 | 29 | 1. [typescript-decorators.ts] contains `bootstrap` method to auto-load all controllers 30 | 31 | 1. [app.js] require Fastify instance from [typescript-decorators.ts] and launch application 32 | 33 | For more details take a look on [fastify-decorators] package. 34 | 35 | **NOTE**: `experimentalDecorators` should be enabled in [typescript config] 36 | 37 | **NOTE**: give a look also to the [`package.json`](./package.json) to find out how the scripts are done 😉 38 | 39 | [main.controller.ts]: src/controllers/main.controller.ts 40 | [typescript-decorators.ts]: ./src/typescript-decorators.ts 41 | [typescript config]: ./tsconfig.json 42 | [app.js]: ./bin/app.js 43 | [fastify-decorators]: https://npmjs.org/package/fastify-decorators 44 | -------------------------------------------------------------------------------- /typescript-decorators/bin/app.js: -------------------------------------------------------------------------------- 1 | // Required to get normal stacktrace with references to source (typescript) files 2 | require('source-map-support').install() 3 | 4 | // Require fastify instance 5 | const app = require('../dist/typescript-decorators').instance 6 | 7 | // Start listening on 3000 port 8 | app.listen(3000, (err) => { 9 | if (err) throw err 10 | 11 | console.log(`Application is ready and listening on http://localhost:3000`) 12 | console.log(`Available routes:`) 13 | console.log(app.printRoutes()) 14 | }) 15 | -------------------------------------------------------------------------------- /typescript-decorators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-decorators", 3 | "version": "1.0.0", 4 | "description": "Example how to use typescript decorators to build controllers with handlers and hooks", 5 | "main": "bin/app.js", 6 | "scripts": { 7 | "start": "node bin/app.js", 8 | "try-me": "curl http://localhost:3000/main", 9 | "build": "tsc", 10 | "test": "tap" 11 | }, 12 | "keywords": [ 13 | "typescript", 14 | "fastify" 15 | ], 16 | "author": "Andrey Chalkin (https://github.com/L2jLiga)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "fastify": "^2.11.0", 20 | "fastify-decorators": "^2.0.0-1", 21 | "reflect-metadata": "^0.1.13", 22 | "source-map-support": "^0.5.16" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^12.12.24", 26 | "tap": "^14.10.5", 27 | "typescript": "^3.7.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /typescript-decorators/src/controllers/main.controller.ts: -------------------------------------------------------------------------------- 1 | import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' 2 | import { Controller, ControllerType, FastifyInstanceToken, GET, Hook, Inject, POST } from 'fastify-decorators' 3 | import { IncomingMessage, ServerResponse } from 'http' 4 | import { MessageService } from '../services/message.service'; 5 | 6 | // Define controller 7 | @Controller({ 8 | route: '/main', // Base URL for all controller handlers 9 | type: ControllerType.SINGLETON // SINGLETON is default controller type, just define it explicit 10 | }) 11 | class MainController { 12 | @Inject(FastifyInstanceToken) 13 | private instance!: FastifyInstance 14 | 15 | constructor(private messageService: MessageService) { 16 | } 17 | 18 | // Creates controller's GET handler which will return message, actually parameters are not required but kept for simplicity 19 | @GET({url: '/'}) 20 | public async returnLastInputValue( 21 | req: FastifyRequest, 22 | reply: FastifyReply 23 | ) { 24 | 25 | return { message: this.messageService.message } 26 | } 27 | 28 | // Creates controller's POST handler which will store message 29 | @POST({url: '/'}) 30 | public async storeInputMessage( 31 | req: FastifyRequest, 32 | reply: FastifyReply 33 | ) { 34 | const value = req.body.message 35 | 36 | // Using Fastify instance to log that field was changed 37 | this.instance.log.info(`New value received: ${value}`) 38 | 39 | // Storing message in service 40 | this.messageService.message = value 41 | 42 | // Reply with OK 43 | return { message: 'OK' } 44 | } 45 | 46 | // Creates controller's hook (Fastify Hooks) 47 | @Hook('onSend') 48 | public async changeXPoweredBy( 49 | req: FastifyRequest, 50 | reply: FastifyReply 51 | ) { 52 | 53 | reply.header('X-Powered-By', 'Apache') 54 | } 55 | } 56 | 57 | export = MainController 58 | -------------------------------------------------------------------------------- /typescript-decorators/src/services/message.service.ts: -------------------------------------------------------------------------------- 1 | import { Service } from 'fastify-decorators'; 2 | 3 | @Service() 4 | export class MessageService { 5 | // Field to store and read message 6 | private _message: string = '' 7 | 8 | public get message(): string { 9 | return this._message 10 | } 11 | 12 | public set message(value: string) { 13 | this._message = value 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /typescript-decorators/src/typescript-decorators.ts: -------------------------------------------------------------------------------- 1 | // Import reflect metadata for dependency injection mechanism 2 | import 'reflect-metadata' 3 | 4 | import fastify from 'fastify' 5 | import { bootstrap } from 'fastify-decorators' 6 | 7 | const instance = fastify() 8 | 9 | // Register bootstrap to autoload all controllers 10 | instance.register(bootstrap, { 11 | directory: __dirname, // Controllers directory path 12 | 13 | mask: /\.controller\./gi, // Mask to determine controllers 14 | }) 15 | 16 | export { instance } 17 | -------------------------------------------------------------------------------- /typescript-decorators/test/main.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { instance } from '../src/typescript-decorators' 2 | 3 | const tap = require('tap') 4 | 5 | tap.test('Controller hook should change X-Powered-By header', async (t: any) => { 6 | const {headers} = await instance.inject({ 7 | url: '/main/', 8 | method: 'GET' 9 | }) 10 | 11 | t.match(headers, {'x-powered-by': 'Apache'}) 12 | }) 13 | 14 | tap.test(`Controller should keep state`, async (t: any) => { 15 | firstGetRequestShouldReturnEmptyMessage: { 16 | const {payload} = await instance.inject({ 17 | url: '/main/', 18 | method: 'GET' 19 | }) 20 | 21 | t.match(payload, `{"message":""}`) 22 | } 23 | 24 | postRequestShouldStoreMessage: { 25 | const {payload} = await instance.inject({ 26 | url: '/main/', 27 | method: 'POST', 28 | payload: { message: 'test' } 29 | }) 30 | 31 | t.match(payload, `{"message":"OK"}`) 32 | } 33 | 34 | secondGetRequestShouldReturnStoredMessage: { 35 | const {payload} = await instance.inject({ 36 | url: '/main/', 37 | method: 'GET' 38 | }) 39 | 40 | t.match(payload, `{"message":"test"}`) 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /typescript-decorators/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "outDir": "./dist", 8 | "rootDir": "./src", 9 | 10 | /* Strict Type-Checking Options */ 11 | "strict": true, 12 | 13 | /* Module Resolution Options */ 14 | "esModuleInterop": true, 15 | 16 | /* Source Map Options */ 17 | "inlineSourceMap": true, 18 | "inlineSources": true, 19 | 20 | /* Experimental Options */ 21 | "experimentalDecorators": true, 22 | "emitDecoratorMetadata": true 23 | }, 24 | "exclude": [ 25 | "test" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /validation-messages/README.md: -------------------------------------------------------------------------------- 1 | # example of custom validation errors using AJV and Fastify v3 2 | 3 | This example show you how to customize the errors messages through input schema validation. 4 | 5 | Check now the [source code](./custom-errors-messages.js)! You will find all the info there in the comments. 6 | There are also [an example](./custom-errors-messages.test.js) that explain how to write the tests! 7 | 8 | You can run this project simply by: 9 | 10 | ```sh 11 | cd validation-messages 12 | npm i 13 | npm start 14 | ``` 15 | 16 | **NOTE**: give a look also to the [`package.json`](./package.json) to find out how the scripts are done 😉 17 | -------------------------------------------------------------------------------- /validation-messages/custom-errors-messages.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Ajv = require('ajv') 4 | const AjvErrors = require('ajv-errors') 5 | 6 | module.exports = function (fastify, options, next) { 7 | const ajv = new Ajv({ allErrors: true, jsonPointers: true }) 8 | // enhance the ajv instance 9 | AjvErrors(ajv) 10 | 11 | // create a schema to validate the input, using the specs by the `ajv-errors` module 12 | const schema = { 13 | $id: 'hello', 14 | type: 'object', 15 | required: ['name'], 16 | properties: { 17 | name: { 18 | type: 'string' 19 | } 20 | }, 21 | additionalProperties: false, 22 | errorMessage: 'my custom message' 23 | } 24 | 25 | fastify.get('/', { 26 | schema: { 27 | querystring: schema 28 | }, 29 | validatorCompiler: ({ schema }) => { 30 | const validate = ajv.compile(schema) 31 | return validate 32 | } 33 | }, (req, res) => res.send('hello')) 34 | 35 | next() 36 | } 37 | -------------------------------------------------------------------------------- /validation-messages/custom-errors-messages.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('node:test') 4 | const Fastify = require('fastify') 5 | const plugin = require('./custom-errors-messages') 6 | 7 | test('test the custom error message response', async t => { 8 | t.plan(2) 9 | 10 | // create a fastify instance to run 11 | const fastify = Fastify() 12 | fastify.register(plugin) 13 | 14 | // usa this spacial method that simulate a HTTP request without starting the server!! 15 | const res = await fastify.inject({ 16 | method: 'GET', 17 | url: '/' 18 | }) 19 | 20 | // validate the output 21 | t.assert.deepStrictEqual(res.statusCode, 400) 22 | t.assert.deepStrictEqual(res.json().message, 'querystring my custom message') 23 | }) 24 | -------------------------------------------------------------------------------- /validation-messages/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "validation-messages", 3 | "version": "1.0.0", 4 | "description": "example of custom validation errors using AJV and Fastify v2", 5 | "main": "custom-errors-messages.js", 6 | "scripts": { 7 | "start": "fastify start custom-errors-messages.js -l debug -P", 8 | "call": "curl http://localhost:3000/", 9 | "test": "standard && node --test" 10 | }, 11 | "keywords": [], 12 | "author": "Manuel Spigolon (https://github.com/Eomm)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "ajv": "^8.17.1", 16 | "ajv-errors": "^3.0.0" 17 | }, 18 | "devDependencies": { 19 | "fastify": "^5.2.1", 20 | "fastify-cli": "^7.3.0", 21 | "standard": "^17.1.2" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /winston-logger/README.md: -------------------------------------------------------------------------------- 1 | # example how to use winston logger with Fastify v2 2 | 3 | This example shows you how to customize logger. 4 | 5 | Check now the [source code](winston-logger.js)! You will find all the info there in the comments. 6 | 7 | You can run this project simply by: 8 | 9 | ```sh 10 | cd winston-logger 11 | npm i 12 | npm start 13 | ``` 14 | 15 | **NOTE**: give a look also to the [`package.json`](./package.json) to find out how the scripts are done 😉 16 | -------------------------------------------------------------------------------- /winston-logger/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "winston-logger", 3 | "version": "1.0.0", 4 | "description": "example how to use winston logger with Fastify v2", 5 | "main": "winston-logger.js", 6 | "scripts": { 7 | "start": "node winston-logger.js", 8 | "try-me": "curl http://localhost:3000/hello", 9 | "test": "standard" 10 | }, 11 | "keywords": [], 12 | "author": "Andrey Chalkin (https://github.com/L2jLiga)", 13 | "license": "MIT", 14 | "dependencies": { 15 | "fastify": "^5.2.1", 16 | "winston": "^3.17.0" 17 | }, 18 | "devDependencies": { 19 | "standard": "^17.1.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /winston-logger/winston-logger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const winston = require('winston') 4 | 5 | // Create custom logger 6 | const loggerInstance = winston.createLogger({ 7 | // Define levels required by Fastify (by default winston has verbose level and does not have trace) 8 | levels: { 9 | fatal: 0, 10 | error: 1, 11 | warn: 2, 12 | info: 3, 13 | trace: 4, 14 | debug: 5 15 | }, 16 | // Setup log level 17 | level: 'info', 18 | // Setup logs format 19 | format: winston.format.json(), 20 | // Define transports to write logs, it could be http, file or console 21 | transports: [ 22 | new winston.transports.File({ filename: 'error.log', level: 'error' }), 23 | new winston.transports.File({ filename: 'combined.log' }) 24 | ] 25 | }) 26 | 27 | // create a fastify instance to run and specify logger 28 | const fastify = require('fastify')({ loggerInstance }) 29 | 30 | fastify.get('/hello', (req, reply) => { 31 | fastify.log.info('Sending hello') 32 | 33 | reply.send({ greet: 'hello' }) 34 | }) 35 | 36 | fastify.setNotFoundHandler((request, reply) => { 37 | fastify.log.debug('Route not found: ', request.req.url) 38 | 39 | reply.status(404).send({ message: 'Not found' }) 40 | }) 41 | 42 | fastify.setErrorHandler((error, request, reply) => { 43 | fastify.log.debug(`Request url: `, request.req.url) 44 | fastify.log.debug(`Payload: `, request.body) 45 | fastify.log.error(`Error occurred: `, error) 46 | 47 | reply.status(500).send({ message: 'Error occurred during request' }) 48 | }) 49 | 50 | fastify.listen({port: 3000}) 51 | --------------------------------------------------------------------------------