├── README.md └── examples-in-class └── web-api ├── README.md ├── database └── data.json ├── layers.txt ├── package.json ├── run-api.sh ├── src ├── entities │ └── hero.js ├── factories │ └── heroFactory.js ├── handler.js ├── index.js ├── repositories │ └── heroRepository.js ├── routes │ └── heroRoute.js ├── services │ └── heroService.js └── util │ └── util.js └── tests ├── integration └── hero.test.js └── unit └── routes └── heroRoute.test.js /README.md: -------------------------------------------------------------------------------- 1 | # Building a complete Node.js WebApi + testing with no frameworks 2 | 3 | Welcome, this repo is part of my youtube video about Creating and testing a complete Node.js Rest API (With no frameworks). 4 | 5 | First of all, leave your star 🌟 on this repo. 6 | 7 | Access our [**exclusive telegram channel**](https://bit.ly/ErickWendelContentHub) so I'll let you know about all the content I've been producing 8 | 9 | ## Source code of the examples showed in class 10 | - Access them in [examples-in-class/web-api](./examples-in-class/web-api) 11 | ## Features Checklist + Challenges 12 | 13 | - Web API 14 | - [ ] it should have an endpoint for storing heroes' data 15 | - [ ] it should have an endpoint for retrieving heroes' data 16 | - [ ] it should have an endpoint for updating heroes' data 17 | - [ ] it should have an endpoint for deleting heroes' data 18 | - [ ] it should test when the application throws an error 19 | 20 | - Testing 21 | - Unit 22 | - [ ] it should test all files on the routes layer 23 | - [ ] it should test all files on the repositories layer 24 | - [ ] it should test all files on the factories layer 25 | - Plus 26 | - [ ] it should reach 100% code coverage (it's currently not possible to get code coverage metrics using only the native Node.js, see [c8](https://www.npmjs.com/package/c8) for this task) 27 | 28 | - Integration / E2E 29 | - [ ] it should test the endpoint for storing heroes' data 30 | - [ ] it should test the endpoint for retrieving heroes' data 31 | - [ ] it should test the endpoint for updating heroes' data 32 | - [ ] it should test the endpoint for deleting heroes' data 33 | - [ ] it should test when the application throws an error 34 | 35 | ### Notes 36 | - Should you have some difficulties solving the problems, please comment on the [**Youtube video**](https://youtu.be/xR4D2bp8_S0) 37 | 38 | - As soon as you've been finishing the tasks, comment on the [**Youtube video**](https://youtu.be/xR4D2bp8_S0) so all other students can be pushed forward by your efforts 39 | 40 | ## Have fun! 41 | -------------------------------------------------------------------------------- /examples-in-class/web-api/README.md: -------------------------------------------------------------------------------- 1 | # Building a complete Node.js WebApi + testing with no frameworks 2 | 3 | Welcome, this repo is part of my youtube video about Creating and testing a complete Node.js Rest API (With no frameworks). 4 | 5 | First of all, leave your star 🌟 on this repo. 6 | 7 | Access our [**exclusive telegram channel**](https://bit.ly/ErickWendelContentHub) so I'll let you know about all the content I've been producing 8 | 9 | ## Features Checklist + Challenges 10 | 11 | - Web API 12 | - [x] it should have an endpoint for storing heroes' data 13 | - [x] it should have an endpoint for retrieving heroes' data 14 | - [ ] it should have an endpoint for updating heroes' data 15 | - [ ] it should have an endpoint for deleting heroes' data 16 | 17 | - Testing 18 | - Unit 19 | - [ ] it should test when the application throws an error 20 | - [x] it should test all files on the routes layer 21 | - [ ] it should test all files on the repositories layer 22 | - [ ] it should test all files on the factories layer 23 | - Plus 24 | - [ ] it should reach 100% code coverage (it's currently not possible to get code coverage metrics using only the native Node.js, see [c8](https://www.npmjs.com/package/c8) for this task) 25 | 26 | - Integration / E2E 27 | - [x] it should test the endpoint for storing heroes' data 28 | - [ ] it should test the endpoint for retrieving heroes' data 29 | - [ ] it should test the endpoint for updating heroes' data 30 | - [ ] it should test the endpoint for deleting heroes' data 31 | - [ ] it should test when the application throws an error 32 | 33 | ### Notes 34 | - Should you have some difficulties solving the problems, please comment on the [**Youtube video**](#) 35 | 36 | - As soon as you've been finishing the tasks, comment on the [**Youtube video**](#) so all other students can be pushed forward by your efforts 37 | 38 | ## Have fun! 39 | -------------------------------------------------------------------------------- /examples-in-class/web-api/database/data.json: -------------------------------------------------------------------------------- 1 | [{"id":"90bf10a3-c9fb-406a-a35a-3e4a8db0fbf8","name":"Batman","age":50,"power":"rich"},{"id":"cb506c7d-1c2f-4ce8-a56f-d45f16bf34cf","name":"Batman","age":50,"power":"rich"},{"id":"6bec69a2-2c42-4793-bdbb-cc4aa01509c8","name":"Flash","age":99,"power":"speed"}] -------------------------------------------------------------------------------- /examples-in-class/web-api/layers.txt: -------------------------------------------------------------------------------- 1 | N-Layers 2 | 3 | database 4 | - a file which store all application data 5 | 6 | src -all source code 7 | - entities - object mappings 8 | - factories - instance generators 9 | - repositories - data acess 10 | - routes - endpoint mappings 11 | - services - commmunication between the routes and repositories layer (business logic) 12 | - util - shared code 13 | - handler - commmunication between routes and server 14 | - index - server instance 15 | 16 | tests -> all automated test suites 17 | - integration tests - testing on the user point of view. it's also an E2E test because there's no app consuming it 18 | 19 | - unit tests 20 | all tests that must run wihtout any external connections such as 21 | databases, external APIs and on our case, the fileSystem -------------------------------------------------------------------------------- /examples-in-class/web-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-api", 3 | "version": "0.0.1", 4 | "description": "Welcome, this repo is part of my youtube video about Creating and testing a complete Node.js Rest API (With no frameworks).", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node src/index.js", 9 | "test": "node --test tests/" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /examples-in-class/web-api/run-api.sh: -------------------------------------------------------------------------------- 1 | curl --silent localhost:3000/heroes 2 | # "results":[{"id":"90bf10a3-c9fb-406a-a35a-3e4a8db0fbf8","name":"Batman","age":50,"power":"rich"},{"id":"cb506c7d-1c2f-4ce8-a56f-d45f16bf34cf","name":"Batman","age":50,"power":"rich"}]} 3 | 4 | curl \ 5 | --silent \ 6 | -X POST \ 7 | -d '{"name": "Flash", "age": 99, "power": "speed"}' \ 8 | localhost:3000/heroes 9 | # {"id":"6bec69a2-2c42-4793-bdbb-cc4aa01509c8","success":"User created with success!!"} 10 | 11 | curl \ 12 | --silent \ 13 | -X POST \ 14 | -d '{"invalid json payload"}' \ 15 | localhost:3000/heroes 16 | 17 | # {"error":"internet server error!!"}% -------------------------------------------------------------------------------- /examples-in-class/web-api/src/entities/hero.js: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto' 2 | export default class Hero { 3 | constructor({ name, age, power }) { 4 | this.id = randomUUID() 5 | this.name = name 6 | this.age = age 7 | this.power = power 8 | } 9 | } -------------------------------------------------------------------------------- /examples-in-class/web-api/src/factories/heroFactory.js: -------------------------------------------------------------------------------- 1 | import HeroRepository from "../repositories/heroRepository.js" 2 | import HeroService from "../services/heroService.js" 3 | 4 | const generateInstance = ({ 5 | filePath 6 | }) => { 7 | // hero goes all db connections 8 | const heroRepository = new HeroRepository({ 9 | file: filePath 10 | }) 11 | const heroService = new HeroService({ 12 | heroRepository 13 | }) 14 | 15 | return heroService 16 | } 17 | 18 | export { 19 | generateInstance 20 | } -------------------------------------------------------------------------------- /examples-in-class/web-api/src/handler.js: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | join, 4 | dirname 5 | } from 'node:path' 6 | 7 | import { 8 | fileURLToPath 9 | } from 'node:url' 10 | 11 | import { 12 | parse 13 | } from 'node:url' 14 | import { routes } from './routes/heroRoute.js' 15 | import { 16 | DEFAULT_HEADER 17 | } from './util/util.js' 18 | 19 | import { generateInstance } from './factories/heroFactory.js' 20 | 21 | const currentDir = dirname( 22 | fileURLToPath( 23 | import.meta.url 24 | ) 25 | ) 26 | const filePath = join(currentDir, './../database', 'data.json') 27 | 28 | const heroService = generateInstance({ 29 | filePath 30 | }) 31 | 32 | const heroRoutes = routes({ 33 | heroService 34 | }) 35 | 36 | const allRoutes = { 37 | ...heroRoutes, 38 | 39 | // 404 routes 40 | default: (request, response) => { 41 | response.writeHead(404, DEFAULT_HEADER) 42 | response.write('uuuuups, not found!') 43 | response.end() 44 | } 45 | } 46 | 47 | function handler(request, response) { 48 | const { 49 | url, 50 | method 51 | } = request 52 | 53 | const { 54 | pathname 55 | } = parse(url, true) 56 | 57 | const key = `${pathname}:${method.toLowerCase()}` 58 | const chosen = allRoutes[key] || allRoutes.default 59 | 60 | return Promise.resolve(chosen(request, response)) 61 | .catch(handlerError(response)) 62 | } 63 | 64 | function handlerError(response) { 65 | return error => { 66 | console.log('Something bad has happened**', error.stack) 67 | response.writeHead(500, DEFAULT_HEADER) 68 | response.write(JSON.stringify({ 69 | error: 'internet server error!!' 70 | })) 71 | 72 | return response.end() 73 | } 74 | } 75 | 76 | export default handler -------------------------------------------------------------------------------- /examples-in-class/web-api/src/index.js: -------------------------------------------------------------------------------- 1 | import http from 'node:http' 2 | import handler from './handler.js' 3 | 4 | const PORT = process.env.PORT || 3000 5 | 6 | const server = http.createServer(handler) 7 | .listen(PORT, () => console.log(`server is running at ${PORT}`)) 8 | 9 | export { 10 | server 11 | } -------------------------------------------------------------------------------- /examples-in-class/web-api/src/repositories/heroRepository.js: -------------------------------------------------------------------------------- 1 | import { 2 | readFile, 3 | writeFile 4 | } from 'node:fs/promises' 5 | 6 | export default class HeroRepository { 7 | constructor({ 8 | file 9 | }) { 10 | this.file = file 11 | } 12 | 13 | async #currentFileContent() { 14 | return JSON.parse(await readFile(this.file)) 15 | } 16 | 17 | find(){ 18 | return this.#currentFileContent() 19 | } 20 | 21 | async create(data) { 22 | const currentFile = await this.#currentFileContent() 23 | currentFile.push(data) 24 | 25 | await writeFile( 26 | this.file, 27 | JSON.stringify(currentFile) 28 | ) 29 | 30 | return data.id 31 | } 32 | 33 | } 34 | /* 35 | const heroRepository = new HeroRepository({ 36 | file: './../database/data.json' 37 | }) 38 | 39 | console.log( 40 | await heroRepository.create({ 41 | id: 2, 42 | name: 'Chapolin' 43 | }) 44 | ) 45 | console.log( 46 | await heroRepository.find() 47 | ) 48 | 49 | */ -------------------------------------------------------------------------------- /examples-in-class/web-api/src/routes/heroRoute.js: -------------------------------------------------------------------------------- 1 | import { 2 | once 3 | } from 'node:events' 4 | import Hero from '../entities/hero.js' 5 | import { 6 | DEFAULT_HEADER 7 | } from '../util/util.js' 8 | 9 | const routes = ({ 10 | heroService 11 | }) => ({ 12 | '/heroes:get': async (request, response) => { 13 | const heroes = await heroService.find() 14 | 15 | response.write(JSON.stringify({ 16 | results: heroes 17 | })) 18 | return response.end() 19 | }, 20 | 21 | '/heroes:post': async (request, response) => { 22 | const data = await once(request, 'data') 23 | const item = JSON.parse(data) 24 | const hero = new Hero(item) 25 | 26 | const id = await heroService.create(hero) 27 | 28 | response.writeHead(201, DEFAULT_HEADER) 29 | response.write(JSON.stringify({ 30 | id, 31 | success: 'User created with success!!', 32 | })) 33 | 34 | return response.end() 35 | }, 36 | }) 37 | 38 | export { 39 | routes 40 | } -------------------------------------------------------------------------------- /examples-in-class/web-api/src/services/heroService.js: -------------------------------------------------------------------------------- 1 | export default class HeroService { 2 | constructor({ 3 | heroRepository 4 | }) { 5 | this.heroRepository = heroRepository 6 | } 7 | 8 | find() { 9 | return this.heroRepository.find() 10 | } 11 | 12 | create(data) { 13 | return this.heroRepository.create(data) 14 | } 15 | } -------------------------------------------------------------------------------- /examples-in-class/web-api/src/util/util.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_HEADER = { 'content-type': 'application/json'} 2 | 3 | export { 4 | DEFAULT_HEADER 5 | } -------------------------------------------------------------------------------- /examples-in-class/web-api/tests/integration/hero.test.js: -------------------------------------------------------------------------------- 1 | import test from 'node:test' 2 | import assert from 'node:assert' 3 | import { promisify } from 'node:util' 4 | test('Hero Integration Test Suite', async (t) => { 5 | const testPort = 9009 6 | 7 | // that's bad practice because it mutates the environment 8 | process.env.PORT = testPort 9 | const { server } = await import('../../src/index.js') 10 | 11 | const testServerAddress = `http://localhost:${testPort}/heroes` 12 | 13 | await t.test('it should create a hero', async (t) => { 14 | const data = { 15 | name: "Batman", 16 | age: 50, 17 | power: "rich" 18 | } 19 | 20 | const request = await fetch(testServerAddress, { 21 | method: 'POST', 22 | body: JSON.stringify(data) 23 | }) 24 | 25 | assert.deepStrictEqual( 26 | request.headers.get('content-type'), 27 | 'application/json' 28 | ) 29 | 30 | assert.strictEqual(request.status, 201) 31 | 32 | const result = await request.json() 33 | assert.deepStrictEqual( 34 | result.success, 35 | 'User created with success!!', 36 | 'it should return a valid text message' 37 | ) 38 | 39 | assert.ok( 40 | result.id.length > 30, 41 | 'id should be a valid uuid' 42 | ) 43 | 44 | 45 | }) 46 | 47 | await promisify(server.close.bind(server))() 48 | 49 | }) -------------------------------------------------------------------------------- /examples-in-class/web-api/tests/unit/routes/heroRoute.test.js: -------------------------------------------------------------------------------- 1 | import test from 'node:test' 2 | import assert from 'node:assert' 3 | const callTracker = new assert.CallTracker() 4 | process.on('exit', () => callTracker.verify()) 5 | 6 | import { 7 | routes 8 | } from './../../../src/routes/heroRoute.js' 9 | import { DEFAULT_HEADER } from '../../../src/util/util.js' 10 | 11 | test('Hero routes - endpoints test suite', async (t) => { 12 | await t.test('it should call /heroes:get route', async () => { 13 | const databaseMock = [{ 14 | "id": "90bf10a3-c9fb-406a-a35a-3e4a8db0fbf8", 15 | "name": "Batman", 16 | "age": 50, 17 | "power": "rich" 18 | }] 19 | 20 | const heroServiceStub = { 21 | find: async () => databaseMock 22 | } 23 | 24 | const endpoints = routes({ 25 | heroService: heroServiceStub 26 | }) 27 | 28 | const endpoint = '/heroes:get' 29 | const request = {} 30 | const response = { 31 | write: callTracker.calls(item => { 32 | const expected = JSON.stringify({ 33 | results: databaseMock 34 | }) 35 | assert.strictEqual( 36 | item, 37 | expected, 38 | 'write should be called with the correct payload' 39 | ) 40 | }), 41 | end: callTracker.calls(item => { 42 | assert.strictEqual( 43 | item, 44 | undefined, 45 | 'end should be called without params' 46 | ) 47 | }) 48 | } 49 | const route = endpoints[endpoint] 50 | await route(request, response) 51 | 52 | }) 53 | await t.todo('it should call /heroes:post route') 54 | }) --------------------------------------------------------------------------------