├── .env ├── .gitignore ├── README.md ├── __tests__ └── aceptance │ └── users.test.ts ├── docker-compose.yml ├── jest.config.js ├── ormconfig.json ├── package-lock.json ├── package.json ├── src ├── app.ts ├── config.ts ├── controller │ └── UserController.ts ├── entity │ └── User.ts ├── index.ts └── routes.ts └── tsconfig.json /.env: -------------------------------------------------------------------------------- 1 | PORT=3000 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | node_modules/ 4 | build/ 5 | tmp/ 6 | temp/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typescript-postgres-typeorm 2 | 3 | This is part of a [YouTube series where we're building a REST API using TypeScript & Postgres](https://www.youtube.com/playlist?list=PLdk2EmelRVLpIdCFolrwdLhCTHyeefU6W). 4 | 5 | ![Text placeholder (26)](https://user-images.githubusercontent.com/17026751/144744647-de7d0691-d109-41dc-ab5d-59d3b412cc0d.png) 6 | 7 | 1. [Getting the project started](https://youtu.be/MX3hlSgBLTI). 8 | 2. [Improving developer productivity](https://youtu.be/rflZhPzr_G4): 9 | 1. Setting up developer mode (hot reloading). 10 | 2. Improving routing with async/await. 11 | 3. Adding middleware to handle errors. 12 | 4. Setting up app configuration. 13 | 5. Setting up API logging. 14 | 3. [Validating requests](https://youtu.be/QR-oi1PCaZk). 15 | 4. [Acceptance/end-to-end Tests](https://youtu.be/Ml51d87uoPo). 16 | 17 | ## Prerequisites 18 | 19 | - nodeJS 20 | - docker 21 | 22 | ## Running the Project 23 | 24 | 1. Run `npm i` command 25 | 2. Run `docker compose up -d` command 26 | 3. Run `npm start` command 27 | -------------------------------------------------------------------------------- /__tests__/aceptance/users.test.ts: -------------------------------------------------------------------------------- 1 | import { createConnection } from "typeorm"; 2 | import * as request from 'supertest'; 3 | import app from "../../src/app"; 4 | import { port } from "../../src/config"; 5 | 6 | let connection, server; 7 | 8 | const testUser = { 9 | firstName: 'John', 10 | lastName: 'Doe', 11 | age: 20, 12 | }; 13 | 14 | beforeEach(async() => { 15 | connection = await createConnection(); 16 | await connection.synchronize(true); 17 | server = app.listen(port); 18 | }); 19 | 20 | afterEach(() => { 21 | connection.close(); 22 | server.close(); 23 | }); 24 | 25 | it('should be no users initially', async() => { 26 | const response = await request(app).get('/users'); 27 | expect(response.statusCode).toBe(200); 28 | expect(response.body).toEqual([]); 29 | }); 30 | 31 | it('should create a user', async() => { 32 | const response = await request(app).post('/users').send(testUser); 33 | expect(response.statusCode).toBe(200); 34 | expect(response.body).toEqual({ ...testUser, id: 1 }); 35 | }); 36 | 37 | it('should not create a user if no firstName is given', async() => { 38 | const response = await request(app).post('/users').send({ lastName: 'Doe', age: 21 }); 39 | expect(response.statusCode).toBe(400); 40 | expect(response.body.errors).not.toBeNull(); 41 | expect(response.body.errors.length).toBe(1); 42 | expect(response.body.errors[0]).toEqual({ 43 | msg: 'Invalid value', param: 'firstName', location: 'body' 44 | }); 45 | }); 46 | 47 | it('should not create a user if age is less than 0', async() => { 48 | const response = await request(app).post('/users').send({ firstName: 'John', lastName: 'Doe', age: -1 }); 49 | expect(response.statusCode).toBe(400); 50 | expect(response.body.errors).not.toBeNull(); 51 | expect(response.body.errors.length).toBe(1); 52 | expect(response.body.errors[0]).toEqual({ 53 | msg: 'age must be a positive integer', param: 'age', value: -1, location: 'body', 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | postgres: 4 | image: postgres 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: postgres 8 | ports: 9 | - "5432:5432" 10 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | -------------------------------------------------------------------------------- /ormconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "postgres", 3 | "host": "localhost", 4 | "port": 5432, 5 | "username": "postgres", 6 | "password": "postgres", 7 | "database": "postgres", 8 | "synchronize": true, 9 | "logging": false, 10 | "entities": [ 11 | "src/entity/**/*.ts" 12 | ], 13 | "migrations": [ 14 | "src/migration/**/*.ts" 15 | ], 16 | "subscribers": [ 17 | "src/subscriber/**/*.ts" 18 | ], 19 | "cli": { 20 | "entitiesDir": "src/entity", 21 | "migrationsDir": "src/migration", 22 | "subscribersDir": "src/subscriber" 23 | } 24 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "new-typeorm-project", 3 | "version": "0.0.1", 4 | "description": "Awesome project developed with TypeORM.", 5 | "devDependencies": { 6 | "@types/jest": "^27.0.3", 7 | "@types/node": "^8.0.29", 8 | "jest": "^27.4.3", 9 | "nodemon": "^2.0.15", 10 | "supertest": "^6.1.6", 11 | "ts-jest": "^27.1.0", 12 | "ts-node": "3.3.0", 13 | "typescript": "^3.3.3333" 14 | }, 15 | "dependencies": { 16 | "body-parser": "^1.18.1", 17 | "dotenv": "^10.0.0", 18 | "express": "^4.15.4", 19 | "express-validator": "^6.13.0", 20 | "morgan": "^1.10.0", 21 | "pg": "^8.4.0", 22 | "reflect-metadata": "^0.1.10", 23 | "typeorm": "0.2.40" 24 | }, 25 | "scripts": { 26 | "start": "ts-node src/index.ts", 27 | "dev": "nodemon -w *.ts -w .env src/index.ts", 28 | "typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js", 29 | "test": "jest" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | import * as express from "express"; 2 | import * as bodyParser from "body-parser"; 3 | import {Request, Response} from "express"; 4 | import * as morgan from 'morgan'; 5 | import {Routes} from "./routes"; 6 | import { validationResult } from "express-validator"; 7 | 8 | function handleError(err, _req, res, _next) { 9 | res.status(err.statusCode || 500).send(err.message) 10 | } 11 | 12 | const app = express(); 13 | app.use(morgan('tiny')); 14 | app.use(bodyParser.json()); 15 | 16 | Routes.forEach(route => { 17 | (app as any)[route.method](route.route, 18 | ...route.validation, 19 | async (req: Request, res: Response, next: Function) => { 20 | 21 | try { 22 | const errors = validationResult(req); 23 | if (!errors.isEmpty()) { 24 | return res.status(400).json({ errors: errors.array() }); 25 | } 26 | 27 | const result = await (new (route.controller as any))[route.action](req, res, next); 28 | res.json(result); 29 | } catch(err) { 30 | next(err); 31 | } 32 | }); 33 | }); 34 | 35 | app.use(handleError); 36 | 37 | export default app; 38 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | export const port = process.env.PORT || 3000; 4 | -------------------------------------------------------------------------------- /src/controller/UserController.ts: -------------------------------------------------------------------------------- 1 | import {getRepository} from "typeorm"; 2 | import {NextFunction, Request, Response} from "express"; 3 | import {User} from "../entity/User"; 4 | 5 | export class UserController { 6 | 7 | private userRepository = getRepository(User); 8 | 9 | async all(request: Request, response: Response, next: NextFunction) { 10 | return this.userRepository.find(); 11 | } 12 | 13 | async one(request: Request, response: Response, next: NextFunction) { 14 | return this.userRepository.findOne(request.params.id); 15 | } 16 | 17 | async save(request: Request, response: Response, next: NextFunction) { 18 | return this.userRepository.save(request.body); 19 | } 20 | 21 | async remove(request: Request, response: Response, next: NextFunction) { 22 | let userToRemove = await this.userRepository.findOne(request.params.id); 23 | if (!userToRemove) throw new Error('User not found'); 24 | await this.userRepository.remove(userToRemove); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/entity/User.ts: -------------------------------------------------------------------------------- 1 | import {Entity, PrimaryGeneratedColumn, Column} from "typeorm"; 2 | 3 | @Entity() 4 | export class User { 5 | @PrimaryGeneratedColumn() 6 | id: number; 7 | 8 | @Column() 9 | firstName: string; 10 | 11 | @Column() 12 | lastName: string; 13 | 14 | @Column() 15 | age: number; 16 | } 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "reflect-metadata"; 2 | import { createConnection } from "typeorm"; 3 | import { port } from './config'; 4 | import app from './app'; 5 | 6 | createConnection().then(async connection => { 7 | app.listen(port); 8 | console.log(`Express server has started on port ${port}.`); 9 | }).catch(error => console.log(error)); 10 | -------------------------------------------------------------------------------- /src/routes.ts: -------------------------------------------------------------------------------- 1 | import { body, param } from "express-validator"; 2 | import {UserController} from "./controller/UserController"; 3 | 4 | export const Routes = [{ 5 | method: "get", 6 | route: "/users", 7 | controller: UserController, 8 | action: "all", 9 | validation: [], 10 | }, { 11 | method: "get", 12 | route: "/users/:id", 13 | controller: UserController, 14 | action: "one", 15 | validation: [ 16 | param('id').isInt(), 17 | ], 18 | }, { 19 | method: "post", 20 | route: "/users", 21 | controller: UserController, 22 | action: "save", 23 | validation: [ 24 | body('firstName').isString(), 25 | body('lastName').isString(), 26 | body('age').isInt({ min: 0 }).withMessage('age must be a positive integer'), 27 | ], 28 | }, { 29 | method: "delete", 30 | route: "/users/:id", 31 | controller: UserController, 32 | action: "remove", 33 | validation: [ 34 | param('id').isInt(), 35 | ], 36 | }]; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "es5", 5 | "es6" 6 | ], 7 | "target": "es5", 8 | "module": "commonjs", 9 | "moduleResolution": "node", 10 | "outDir": "./build", 11 | "emitDecoratorMetadata": true, 12 | "experimentalDecorators": true, 13 | "sourceMap": true 14 | } 15 | } --------------------------------------------------------------------------------