Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
nexckycort committed Oct 30, 2020
0 parents commit c63a40c
Show file tree
Hide file tree
Showing 36 changed files with 1,139 additions and 0 deletions.
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
root = true
[*]
end_of_line = crlf
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true
23 changes: 23 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
PORT=3000
NAME_API=API

PGUSER=user
PGHOST=localhost
PGPASSWORD=pass
PGDATABASE=name
PGPORT=5432

EMAILUSER=user
EMAILPASSWORD=pass
EMAILHOST=smtp.mailtrap.io
EMAILPORT=2525

URL_CLIENT=https://client.io

DATABASE_MONGO=mongodb+srv://user:[email protected]/database

SSL=false

LOG_DIR=./logs

SECRETKEY=sdfsd&%efgewr32
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
package-lock.json
.env
build
production
logs/
coverage/
52 changes: 52 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
image: node:12.19.0

stages:
- install
- test
- build
- deploy

install:
stage: install
script:
- npm install
artifacts:
expire_in: 1h
paths:
- node_modules/
cache:
paths:
- node_modules/

tests:
stage: test
dependencies:
- install
script:
- npm run test

build:
stage: build
dependencies:
- install
script:
- npm run build
artifacts:
expire_in: 1h
paths:
- production/
only:
- master

Deploy:
image: ruby:latest
stage: deploy
dependencies:
- build
only:
- master
script:
- apt-get update -qy
- apt-get install -y ruby-dev
- gem install dpl
- dpl --provider=heroku --app=$HEROKU_APP_NAME --api-key=$HEROKU_API_KEY
7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/tests'],
setupFiles: ['<rootDir>/tests/setup.ts'],
collectCoverageFrom: ['<rootDir>/src/**/*.ts', '!**/node_modules/**']
}
66 changes: 66 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"name": "backend-architecture-nodejs",
"version": "1.0.0",
"author": "Nestor Cortina",
"description": "",
"main": "server.js",
"engines": {
"node": "12.19.0",
"npm": "6.14.8",
"typescript": "4.0.3"
},
"scripts": {
"start": "node ./build/server.js",
"dev": "ts-node-dev src/bin/www.ts",
"test": "jest --forceExit --detectOpenHandles --coverage --verbose",
"prebuild": "rm -rf ./prebuild && tsc",
"deletedev": "rm -rf ./prebuild",
"build": "npm run prebuild && npm run overbuild",
"overbuild": "rm -rf build && webpack --config webpack/webpack.config.js -p --env=prod && npm run deletedev"
},
"repository": {
"type": "git",
"url": ""
},
"license": "ISC",
"dependencies": {
"bcrypt": "^5.0.0",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.17.1",
"helmet": "^4.1.1",
"joi": "^17.2.1",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.20",
"mongoose": "^5.10.5",
"morgan": "^1.10.0",
"nodemailer": "^6.4.14",
"pg": "^8.3.3"
},
"devDependencies": {
"@types/compression": "^1.7.0",
"@types/bcrypt": "^3.0.0",
"@types/cors": "^2.8.8",
"@types/express": "^4.17.8",
"@types/http-errors": "^1.8.0",
"@types/jest": "^26.0.15",
"@types/jsonwebtoken": "^8.5.0",
"@types/lodash": "^4.14.162",
"@types/mongoose": "^5.7.36",
"@types/morgan": "^1.9.1",
"@types/node": "^14.14.0",
"@types/nodemailer": "^6.4.0",
"@types/pg": "^7.14.4",
"@types/supertest": "^2.0.10",
"babel-loader": "^8.1.0",
"dotenv": "^8.2.0",
"jest": "^26.6.0",
"supertest": "^5.0.0",
"ts-jest": "^26.4.1",
"ts-node-dev": "^1.0.0",
"typescript": "^4.0.2",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-node-externals": "^2.5.2"
}
}
57 changes: 57 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import express, { Request, Response, NextFunction, Application } from 'express'
import compression from 'compression';
import helmet from 'helmet'
import cors from 'cors';
import bodyParser from 'body-parser'
import morgan from 'morgan'

import routesV1 from './routes/v1'
import { template } from './helpers/template'
import { environment } from './config'
import { pool } from './database/pgPool'
import { ApiError, InternalError, NotFoundError } from './core/ApiError'
import db from './database/mongo';

process.on('uncaughtException', (e) => {
console.log(e)
})

const app: Application = express()

app.use(cors());
app.use(helmet());
app.use(compression());

app.use(bodyParser.json({ limit: '10mb' }))
app.use(bodyParser.urlencoded({ limit: '10mb', extended: true, parameterLimit: 50000 }))

app.use(morgan('dev'))

app.use('/v1', routesV1)
app.get('/', (req: Request, res: Response) => {
res.status(200).send(template('welcome to api'))
})

app.use((req, res, next) => next(new NotFoundError()))

app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof ApiError) {
ApiError.handle(err, res)
} else {
if (environment === 'development') {
console.log(err)
return res.status(500).send(err.message)
}
ApiError.handle(new InternalError(), res)
}
})

pool.connect()
.then()
.catch((error: any) => console.log(`ERROR: ${error.message || error}`))

db.once('open', function () {
console.log('The connection to MongoDB was successful.');
});

export default app
12 changes: 12 additions & 0 deletions src/bin/www.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import http from 'http';
import app from '../app';
import { port } from '../config'

app.set('port', port);

const server = http.createServer(app);

server.listen(port, () => {
console.log(`server running on port ${port}`)
})
.on('error', (e) => console.log(e))
20 changes: 20 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
if (!process.env.NODE_ENV) require('dotenv').config()

export const environment = process.env.NODE_ENV
export const port = process.env.PORT

export const pgUser = process.env.PGUSER
export const pgPassword = process.env.PGPASSWORD
export const pgHost = process.env.PGHOST
export const pgDatabase = process.env.PGDATABASE
export const pgPort = parseInt(process.env.PGPORT)
export const pgSsl = (process.env.SSL === 'false') ? false : true

export const emailUser = process.env.EMAILUSER
export const emailPassword = process.env.EMAILPASSWORD
export const emailHost = process.env.EMAILHOST
export const emailPort = process.env.EMAILPORT

export const urlClient = process.env.URL_CLIENT

export const secretKey = process.env.SECRETKEY
116 changes: 116 additions & 0 deletions src/core/ApiError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Response } from 'express'
import { environment } from '../config'
import {
AuthFailureResponse,
AccessTokenErrorResponse,
InternalErrorResponse,
NotFoundResponse,
BadRequestResponse,
ForbiddenResponse,
} from './ApiResponse'

enum ErrorType {
BAD_TOKEN = 'BadTokenError',
TOKEN_EXPIRED = 'TokenExpiredError',
UNAUTHORIZED = 'AuthFailureError',
ACCESS_TOKEN = 'AccessTokenError',
INTERNAL = 'InternalError',
NOT_FOUND = 'NotFoundError',
NO_ENTRY = 'NoEntryError',
NO_DATA = 'NoDataError',
BAD_REQUEST = 'BadRequestError',
FORBIDDEN = 'ForbiddenError',
}

export abstract class ApiError extends Error {
constructor(public type: ErrorType, public message: string = 'error') {
super(type);
}

public static handle(err: ApiError, res: Response): Response {
switch (err.type) {
case ErrorType.BAD_TOKEN:
case ErrorType.TOKEN_EXPIRED:
case ErrorType.UNAUTHORIZED:
return new AuthFailureResponse(err.message).send(res);
case ErrorType.ACCESS_TOKEN:
return new AccessTokenErrorResponse(err.message).send(res);
case ErrorType.INTERNAL:
return new InternalErrorResponse(err.message).send(res);
case ErrorType.NOT_FOUND:
case ErrorType.NO_ENTRY:
case ErrorType.NO_DATA:
return new NotFoundResponse(err.message).send(res);
case ErrorType.BAD_REQUEST:
return new BadRequestResponse(err.message).send(res);
case ErrorType.FORBIDDEN:
return new ForbiddenResponse(err.message).send(res);
default: {
let message = err.message;
// Do not send failure message in production as it may send sensitive data
if (environment === 'production') message = 'Something wrong happened.';
return new InternalErrorResponse(message).send(res);
}
}
}
}

export class AuthFailureError extends ApiError {
constructor(message = 'Invalid Credentials') {
super(ErrorType.UNAUTHORIZED, message);
}
}

export class InternalError extends ApiError {
constructor(message = 'Internal error') {
super(ErrorType.INTERNAL, message);
}
}

export class BadRequestError extends ApiError {
constructor(message = 'Bad Request') {
super(ErrorType.BAD_REQUEST, message);
}
}

export class NotFoundError extends ApiError {
constructor(message = 'Not Found') {
super(ErrorType.NOT_FOUND, message);
}
}

export class ForbiddenError extends ApiError {
constructor(message = 'Permission denied') {
super(ErrorType.FORBIDDEN, message);
}
}

export class NoEntryError extends ApiError {
constructor(message = "Entry don't exists") {
super(ErrorType.NO_ENTRY, message);
}
}

export class BadTokenError extends ApiError {
constructor(message = 'Token is not valid') {
super(ErrorType.BAD_TOKEN, message);
}
}

export class TokenExpiredError extends ApiError {
constructor(message = 'Token is expired') {
super(ErrorType.TOKEN_EXPIRED, message);
}
}

export class NoDataError extends ApiError {
constructor(message = 'No data available') {
super(ErrorType.NO_DATA, message);
}
}

export class AccessTokenError extends ApiError {
constructor(message = 'Invalid access token') {
super(ErrorType.ACCESS_TOKEN, message);
}
}
Loading

0 comments on commit c63a40c

Please sign in to comment.