Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
kastorcode committed Jan 21, 2024
0 parents commit 76fdccf
Show file tree
Hide file tree
Showing 13 changed files with 425 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build
node_modules
yarn.lock
30 changes: 30 additions & 0 deletions README.md
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>&lt;kastor.code/&gt;</b></big>
</p>
21 changes: 21 additions & 0 deletions package.json
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"
}
}
1 change: 1 addition & 0 deletions res/insomnia.json
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"}]}
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PORT = 3000
32 changes: 32 additions & 0 deletions src/database/data.json
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"
}
]
38 changes: 38 additions & 0 deletions src/entities/hero.ts
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
16 changes: 16 additions & 0 deletions src/factories/heroFactory.ts
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
7 changes: 7 additions & 0 deletions src/index.ts
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
})
49 changes: 49 additions & 0 deletions src/repositories/heroRepository.ts
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
87 changes: 87 additions & 0 deletions src/routes.ts
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
29 changes: 29 additions & 0 deletions src/services/heroService.ts
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
Loading

0 comments on commit 76fdccf

Please sign in to comment.