-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 76fdccf
Showing
13 changed files
with
425 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
build | ||
node_modules | ||
yarn.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
## Web API NodeJS with N-Layer Architecture | ||
|
||
> 👷 Developed by Matheus Ramalho de Oliveira | ||
🔨 Brazilian Software Engineer | ||
🏡 Goiânia, Goiás, Brasil | ||
✉️ [email protected] | ||
👍 [instagram.com/kastorcode](https://instagram.com/kastorcode) | ||
|
||
--- | ||
|
||
<p align="center"> | ||
NodeJS backend written with TypeScript to practice design patterns: N-Layer (structure), Repository (data access), Factory and Dependency Injection (instance generation). Paradigms were also used such as: Clojures, Middlewares and Async Iterators. | ||
</p> | ||
|
||
--- | ||
|
||
### Installation and execution | ||
|
||
1. Make a clone of this repository; | ||
2. Open the project folder in a terminal; | ||
3. Run `yarn` to install dependencies; | ||
4. Run `yarn build` to transpile TypeScript, resolve paths and copy files to build folder; | ||
5. Run `yarn dev` to start the development server at port `3000`; | ||
6. Now you can import the `res/insomnia.json` file into [Insomnia](https://insomnia.rest) and make HTTP requests. | ||
|
||
--- | ||
|
||
<p align="center"> | ||
<big><b><kastor.code/></b></big> | ||
</p> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"scripts": { | ||
"build": "tsc", | ||
"postbuild": "tsc-alias && cp -r src/database build", | ||
"dev": "nodemon build/index.js", | ||
"start": "node build/index.js" | ||
}, | ||
"name": "web-api-nodejs-nlayer", | ||
"version": "0.0.1", | ||
"description": "", | ||
"main": "src/index.ts", | ||
"keywords": [], | ||
"author": "<kastor.code/> Matheus Ramalho de Oliveira", | ||
"license": "ISC", | ||
"devDependencies": { | ||
"@types/node": "14", | ||
"nodemon": "^3.0.3", | ||
"tsc-alias": "^1.8.8", | ||
"typescript": "^5.3.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{"_type":"export","__export_format":4,"__export_date":"2024-01-21T18:51:50.052Z","__export_source":"insomnia.desktop.app:v8.6.0","resources":[{"_id":"req_25c401d1637d4455931659af8961dd40","parentId":"wrk_scratchpad","modified":1705760642404,"created":1705760627777,"url":"{{ _.base_url }}","name":"Root","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnia/8.6.0"}],"authentication":{},"metaSortKey":-1705759486924,"isPrivate":false,"pathParameters":[],"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"wrk_scratchpad","parentId":null,"modified":1705759399840,"created":1705759399840,"name":"Scratch Pad","description":"","scope":"collection","_type":"workspace"},{"_id":"req_f60e96ba36e24981b84566632a3c0074","parentId":"wrk_scratchpad","modified":1705771664284,"created":1705759486974,"url":"{{ _.base_url }}/heroes/1705771191333","name":"Get hero","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnia/8.6.0"}],"authentication":{},"metaSortKey":-1705759486824,"isPrivate":false,"pathParameters":[],"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b074e2f12eca4292a54248d519fb44f3","parentId":"wrk_scratchpad","modified":1705762512557,"created":1705762507895,"url":"{{ _.base_url }}/heroes","name":"Get all heroes","description":"","method":"GET","body":{},"parameters":[],"headers":[{"name":"User-Agent","value":"insomnia/8.6.0"}],"authentication":{},"metaSortKey":-1705759486774,"isPrivate":false,"pathParameters":[],"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_fa76a33636d8423891a18a46fb6489dc","parentId":"wrk_scratchpad","modified":1705771919976,"created":1705768008057,"url":"{{ _.base_url }}/heroes","name":"Create hero","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"name\": \"Hinata Hyuga\",\n \"age\": 16,\n \"power\": \"Chunin\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json"},{"name":"User-Agent","value":"insomnia/8.6.0"}],"authentication":{},"metaSortKey":-1705759486724,"isPrivate":false,"pathParameters":[],"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_99d30891da4bdcebc63947a8fc17f076de878684","parentId":"wrk_scratchpad","modified":1705759765971,"created":1705759441119,"name":"Base Environment","data":{"base_url":"localhost:3000"},"dataPropertyOrder":{"&":["base_url"]},"color":null,"isPrivate":false,"metaSortKey":1705759441119,"_type":"environment"},{"_id":"jar_99d30891da4bdcebc63947a8fc17f076de878684","parentId":"wrk_scratchpad","modified":1705759441125,"created":1705759441125,"name":"Default Jar","cookies":[],"_type":"cookie_jar"}]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const PORT = 3000 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
[ | ||
{ | ||
"id": 1705771191333, | ||
"name": "Naruto Uzumaki", | ||
"age": 13, | ||
"power": "Genin" | ||
}, | ||
{ | ||
"id": 1705771240436, | ||
"name": "Sasuke Uchiha", | ||
"age": 13, | ||
"power": "Genin" | ||
}, | ||
{ | ||
"id": 1705771299814, | ||
"name": "Sakura Haruno", | ||
"age": 13, | ||
"power": "Genin" | ||
}, | ||
{ | ||
"id": 1705771422860, | ||
"name": "Iruka Umino", | ||
"age": 23, | ||
"power": "Chunin" | ||
}, | ||
{ | ||
"id": 1705771548937, | ||
"name": "Kakashi Hatake", | ||
"age": 27, | ||
"power": "Jonin" | ||
} | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
type tHeroProps = { | ||
name : string, | ||
age : number, | ||
power : string | ||
} | ||
|
||
|
||
class Hero { | ||
|
||
public id | ||
private name | ||
private age | ||
private power | ||
|
||
constructor ({ name, age, power } : tHeroProps) { | ||
this.id = Math.floor(Math.random() * 100) + Date.now() | ||
this.name = name | ||
this.age = age | ||
this.power = power | ||
} | ||
|
||
|
||
public isValid () { | ||
const properties = Object.getOwnPropertyNames(this) | ||
const invalid = properties | ||
// @ts-ignore | ||
.map(property => !!this[property] ? null : `${property} is missing`) | ||
.filter(message => !!message) | ||
return { | ||
isValid: invalid.length === 0, | ||
error: invalid | ||
} | ||
} | ||
|
||
} | ||
|
||
|
||
export default Hero |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
import HeroRepository from '~/repositories/heroRepository' | ||
import HeroService from '~/services/heroService' | ||
|
||
|
||
function heroFactory () { | ||
const heroRepository = new HeroRepository({ | ||
file: 'database/data.json' | ||
}) | ||
const heroService = new HeroService({ | ||
heroRepository | ||
}) | ||
return heroService | ||
} | ||
|
||
|
||
export default heroFactory |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { PORT } from '~/config' | ||
import startRoutes from '~/routes' | ||
|
||
|
||
startRoutes({ | ||
port: PORT | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import { readFile, writeFile } from 'fs/promises' | ||
import { join } from 'path' | ||
|
||
import Hero from '~/entities/hero' | ||
|
||
type tHeroRepositoryProps = { | ||
file : string | ||
} | ||
|
||
|
||
class HeroRepository { | ||
|
||
private file : string | ||
|
||
constructor ({ file } : tHeroRepositoryProps) { | ||
this.file = this._getFilePath(file) | ||
} | ||
|
||
|
||
private _getFilePath (file : string) { | ||
return join(__dirname, '/../', file) | ||
} | ||
|
||
|
||
private async _currentFileContent () : Promise<Hero[]> { | ||
return JSON.parse((await readFile(this.file)).toString()) | ||
} | ||
|
||
|
||
public async find (heroId : number) { | ||
const fileContent = await this._currentFileContent() | ||
if (!heroId) { | ||
return fileContent | ||
} | ||
return fileContent.find(({ id }) => id == heroId) | ||
} | ||
|
||
|
||
public async create (hero : Hero) { | ||
const fileContent = await this._currentFileContent() | ||
fileContent.push(hero) | ||
await writeFile(this.file, JSON.stringify(fileContent)) | ||
return hero.id | ||
} | ||
|
||
} | ||
|
||
|
||
export default HeroRepository |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import http from 'http' | ||
|
||
import Hero from '~/entities/hero' | ||
import heroFactory from '~/factories/heroFactory' | ||
|
||
type tStartRoutesProps = { | ||
port : number | ||
} | ||
|
||
type tRequest = http.IncomingMessage & { | ||
query : { id : number } | ||
} | ||
|
||
type tRoutes = { | ||
[key:string] : (request : tRequest, response : http.ServerResponse) => Promise<http.ServerResponse | undefined> | ||
} | ||
|
||
const DEFAULT_HEADER = { | ||
'Content-Type': 'application/json' | ||
} | ||
|
||
const heroService = heroFactory() | ||
|
||
const routes : tRoutes = { | ||
default: async (request, response) => { | ||
response.writeHead(404, DEFAULT_HEADER) | ||
return response.end() | ||
}, | ||
'/heroes:get': async (request, response) => { | ||
const heroes = await heroService.find(request.query.id) | ||
response.write(JSON.stringify({ heroes })) | ||
return response.end() | ||
}, | ||
'/heroes:post': async (request, response) => { | ||
try { | ||
for await (const data of request) { | ||
const hero = new Hero(JSON.parse(data)) | ||
const { isValid, error } = hero.isValid() | ||
if (isValid) { | ||
const id = await heroService.create(hero) | ||
response.writeHead(201, DEFAULT_HEADER) | ||
response.write(JSON.stringify({ id })) | ||
} | ||
else { | ||
response.writeHead(400, DEFAULT_HEADER) | ||
response.write(JSON.stringify({ error })) | ||
} | ||
return response.end() | ||
} | ||
} | ||
catch (error) { | ||
return errorHandler(response)(error) | ||
} | ||
} | ||
} | ||
|
||
|
||
function serverRunningLog ({ port } : tStartRoutesProps) { | ||
console.log('server running at', port) | ||
} | ||
|
||
|
||
function errorHandler (response : http.ServerResponse) { | ||
return function (error : unknown) { | ||
console.error(error) | ||
response.writeHead(500, DEFAULT_HEADER) | ||
return response.end() | ||
} | ||
} | ||
|
||
|
||
function startRoutes ({ port } : tStartRoutesProps) { | ||
// @ts-ignore | ||
const requestListener : http.RequestListener = function (request : tRequest, response) { | ||
const { url, method } = request | ||
const [root, route, id] = (url as string).split('/') | ||
response.writeHead(200, DEFAULT_HEADER) | ||
request.query = { id: Number(id) } | ||
const key = '/'.concat(route, ':', (method as string).toLowerCase()) | ||
const handler = routes[key] || routes.default | ||
return handler(request, response).catch(errorHandler(response)) | ||
} | ||
http.createServer(requestListener).listen(port, () => serverRunningLog({ port })) | ||
} | ||
|
||
|
||
export default startRoutes |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import Hero from '~/entities/hero' | ||
import HeroRepository from '~/repositories/heroRepository' | ||
|
||
type tHeroServiceProps = { | ||
heroRepository : HeroRepository | ||
} | ||
|
||
class HeroService { | ||
|
||
private heroRepository | ||
|
||
constructor ({ heroRepository } : tHeroServiceProps) { | ||
this.heroRepository = heroRepository | ||
} | ||
|
||
|
||
public async find (heroId : number) { | ||
return this.heroRepository.find(heroId) | ||
} | ||
|
||
|
||
public async create (hero : Hero) { | ||
return this.heroRepository.create(hero) | ||
} | ||
|
||
} | ||
|
||
|
||
export default HeroService |
Oops, something went wrong.