├── .gitignore ├── LICENSE ├── README.md ├── __tests__ ├── contact-list.spec.js └── fixtures │ └── fake-contact.js ├── package-lock.json ├── package.json └── src ├── contacts ├── contact-list.js ├── contact.js ├── contacts-endpoint.js ├── contacts-endpoint.spec.js └── index.js ├── db └── index.js ├── helpers ├── adapt-request.js ├── errors.js ├── http-error.js ├── is-valid-email.js ├── required-param.js └── upper-first.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Project dependencies 2 | .cache 3 | node_modules 4 | 5 | # Build directory 6 | /dist 7 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dev Mastery. https://devmastery.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the \"Software\"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Live Code Walkthrough - Designing a clean RESTful API with Node.js (Express + Mongo) 2 | 3 | Watch the companion video at: https://www.youtube.com/watch?v=fy6-LSE_zjI 4 | 5 | ## Prerequisites 6 | 7 | - Node.js 8 | - Mongo DB 9 | - Git 10 | 11 | ## Getting started 12 | 13 | Follow these steps at the command line: 14 | 15 | ### 1. Clone and Install 16 | 17 | ```bash 18 | git clone https://github.com/arcdev1/mm_express_api_example.git 19 | cd mm_express_api_example 20 | npm install 21 | ``` 22 | 23 | ### 2. Start Mongo 24 | 25 | ```bash 26 | mongod 27 | ``` 28 | 29 | ### 3. Run the solution 30 | 31 | ```bash 32 | npm run start 33 | ``` 34 | 35 | ## Troubleshooting 36 | 37 | To get the unique email error to work like in the video, you need to add a unique constraint on the email field in MongoDB as described at: https://docs.mongodb.com/manual/core/index-unique/ 38 | 39 | ### License 40 | 41 | Distributed under the MIT License. See [`LICENSE`](LICENSE) for more information. 42 | -------------------------------------------------------------------------------- /__tests__/contact-list.spec.js: -------------------------------------------------------------------------------- 1 | import makeDb from '../src/db' 2 | import makeContactList from '../src/contacts/contact-list' 3 | import makeFakeContact from './fixtures/fake-contact' 4 | import { UniqueConstraintError } from '../src/helpers/errors' 5 | 6 | const database = makeDb() 7 | const contactList = makeContactList({ database }) 8 | 9 | describe('Contacts Repository', async () => { 10 | beforeAll(() => contactList.remove({})) 11 | 12 | it('adds a contact', async () => { 13 | const dummyContact = makeFakeContact() 14 | const result = await contactList.add(dummyContact) 15 | expect(result.success).toBe(true) 16 | expect(result.created).toHaveProperty('contactId') 17 | expect(result.created).toEqual({ 18 | ...dummyContact, 19 | contactId: result.created.contactId 20 | }) 21 | return contactList.remove(dummyContact) 22 | }) 23 | 24 | it('removes a contact', async () => { 25 | const dummyContact = makeFakeContact() 26 | await contactList.add(dummyContact) 27 | expect(await contactList.remove(dummyContact)).toBe(1) 28 | }) 29 | 30 | it('lists contacts', async () => { 31 | const fake1 = contactList.add(makeFakeContact()) 32 | const fake2 = contactList.add(makeFakeContact()) 33 | const fake3 = contactList.add(makeFakeContact()) 34 | const fakes = await Promise.all([fake1, fake2, fake3]) 35 | const items = await contactList.getItems() 36 | expect(items.length).toBe(3) 37 | await Promise.all(fakes.map(({ created }) => contactList.remove(created))) 38 | }) 39 | 40 | it('supports limits', async () => { 41 | const fake1 = contactList.add(makeFakeContact()) 42 | const fake2 = contactList.add(makeFakeContact()) 43 | const fake3 = contactList.add(makeFakeContact()) 44 | const fakes = await Promise.all([fake1, fake2, fake3]) 45 | const items = await contactList.getItems({ max: 2 }) 46 | expect(items.length).toBe(2) 47 | return Promise.all(fakes.map(({ created }) => contactList.remove(created))) 48 | }) 49 | 50 | it('pages forward', async () => { 51 | const fake1 = contactList.add(makeFakeContact()) 52 | const fake2 = contactList.add(makeFakeContact()) 53 | const fake3 = contactList.add(makeFakeContact()) 54 | const fakes = await Promise.all([fake1, fake2, fake3]) 55 | const items = await contactList.getItems({ 56 | after: fakes[0].created.contactId 57 | }) 58 | expect(items.length).toBe(2) 59 | return Promise.all(fakes.map(({ created }) => contactList.remove(created))) 60 | }) 61 | 62 | it('pages backward', async () => { 63 | const fake1 = contactList.add(makeFakeContact()) 64 | const fake2 = contactList.add(makeFakeContact()) 65 | const fake3 = contactList.add(makeFakeContact()) 66 | const fakes = await Promise.all([fake1, fake2, fake3]) 67 | const items = await contactList.getItems({ 68 | after: fakes[0].created.contactId 69 | }) 70 | expect(items.length).toBe(2) 71 | return Promise.all(fakes.map(({ created }) => contactList.remove(created))) 72 | }) 73 | 74 | it('requires new contacts to have a unique email address', async () => { 75 | expect.assertions(2) 76 | 77 | const dummyContact = makeFakeContact() 78 | // await contactList.remove({ emailAddress: dummyContact.emailAddress }) 79 | await contactList.add(dummyContact) 80 | const duplicateContact = makeFakeContact({ 81 | emailAddress: dummyContact.emailAddress 82 | }) 83 | 84 | await contactList.add(duplicateContact).catch(e => { 85 | expect(e.message).toBe('emailAddress must be unique.') 86 | expect(e).toBeInstanceOf(UniqueConstraintError) 87 | }) 88 | return contactList.remove(dummyContact) 89 | }) 90 | 91 | it('requires new contacts to have a unique contact id', async () => { 92 | expect.assertions(2) 93 | const dummyContact = makeFakeContact() 94 | const { created } = await contactList.add(dummyContact) 95 | const duplicateContact = makeFakeContact({ 96 | contactId: created.contactId 97 | }) 98 | await contactList.add(duplicateContact).catch(e => { 99 | expect(e.message).toBe('contactId must be unique.') 100 | expect(e).toBeInstanceOf(UniqueConstraintError) 101 | }) 102 | return contactList.remove(dummyContact) 103 | }) 104 | 105 | it('finds a contact by id', async () => { 106 | const dummyContact = makeFakeContact() 107 | const { created } = await contactList.add(dummyContact) 108 | const result = await contactList.findById(created) 109 | expect(result).toEqual({ 110 | ...dummyContact, 111 | contactId: created.contactId 112 | }) 113 | return contactList.remove(dummyContact) 114 | }) 115 | 116 | it('finds a contact by email address', async () => { 117 | const dummyContact = makeFakeContact() 118 | await contactList.remove({ emailAddress: dummyContact.emailAddress }) 119 | const { created } = await contactList.add(dummyContact) 120 | const found = await contactList.findByEmail(dummyContact) 121 | expect(found[0]).toEqual({ ...dummyContact, contactId: created.contactId }) 122 | return contactList.remove(dummyContact) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /__tests__/fixtures/fake-contact.js: -------------------------------------------------------------------------------- 1 | import faker from 'faker' 2 | import makeContact from '../../src/contacts/contact' 3 | export default function makeFakeContact (spec = {}) { 4 | return makeContact({ 5 | emailAddress: faker.internet.email(), 6 | firstName: faker.name.firstName(), 7 | lastName: faker.name.lastName(), 8 | ...spec 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-api", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "prestart": "npm run -s build", 9 | "start": "node dist/index.js", 10 | "clean": "rimraf dist", 11 | "build": "npm run clean && mkdir -p dist && babel src -s -D -d dist" 12 | }, 13 | "author": "Bill Sourour (Dev Mastery)", 14 | "license": "MIT", 15 | "dependencies": { 16 | "body-parser": "^1.18.3", 17 | "express": "^4.16.4", 18 | "mongodb": "^3.1.12", 19 | "mongoose": "^5.4.4" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.2.3", 23 | "@babel/core": "^7.2.2", 24 | "@babel/preset-env": "^7.2.3", 25 | "babel-core": "^7.0.0-bridge.0", 26 | "babel-jest": "^23.6.0", 27 | "faker": "^4.1.0", 28 | "jest": "^23.6.0", 29 | "regenerator-runtime": "^0.13.1", 30 | "rimraf": "^2.6.3" 31 | }, 32 | "jest": { 33 | "verbose": true, 34 | "testRegex": ".spec.js" 35 | }, 36 | "babel": { 37 | "presets": [ 38 | [ 39 | "@babel/env", 40 | { 41 | "targets": { 42 | "node": "10.8.0" 43 | } 44 | } 45 | ] 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/contacts/contact-list.js: -------------------------------------------------------------------------------- 1 | import makeContact from './contact' 2 | import { UniqueConstraintError } from '../helpers/errors' 3 | 4 | export default function makeContactList ({ database }) { 5 | return Object.freeze({ 6 | add, 7 | findByEmail, 8 | findById, 9 | getItems, 10 | remove, 11 | replace, 12 | update 13 | }) 14 | 15 | async function getItems ({ max = 100, before, after } = {}) { 16 | const db = await database 17 | const query = {} 18 | if (before || after) { 19 | query._id = {} 20 | query._id = before ? { ...query._id, $lt: db.makeId(before) } : query._id 21 | query._id = after ? { ...query._id, $gt: db.makeId(after) } : query._id 22 | } 23 | 24 | return (await db 25 | .collection('contacts') 26 | .find(query) 27 | .limit(Number(max)) 28 | .toArray()).map(documentToContact) 29 | } 30 | 31 | async function add ({ contactId, ...contact }) { 32 | const db = await database 33 | if (contactId) { 34 | contact._id = db.makeId(contactId) 35 | } 36 | const { result, ops } = await db 37 | .collection('contacts') 38 | .insertOne(contact) 39 | .catch(mongoError => { 40 | const [errorCode] = mongoError.message.split(' ') 41 | if (errorCode === 'E11000') { 42 | const [_, mongoIndex] = mongoError.message.split(':')[2].split(' ') 43 | throw new UniqueConstraintError( 44 | mongoIndex === 'ContactEmailIndex' ? 'emailAddress' : 'contactId' 45 | ) 46 | } 47 | throw mongoError 48 | }) 49 | return { 50 | success: result.ok === 1, 51 | created: documentToContact(ops[0]) 52 | } 53 | } 54 | 55 | async function findById ({ contactId }) { 56 | const db = await database 57 | const found = await db 58 | .collection('contacts') 59 | .findOne({ _id: db.makeId(contactId) }) 60 | if (found) { 61 | return documentToContact(found) 62 | } 63 | return null 64 | } 65 | 66 | async function findByEmail ({ emailAddress }) { 67 | const db = await database 68 | const results = await db 69 | .collection('contacts') 70 | .find({ emailAddress }) 71 | .toArray() 72 | return results.map(documentToContact) 73 | } 74 | 75 | async function remove ({ contactId, ...contact }) { 76 | const db = await database 77 | if (contactId) { 78 | contact._id = db.makeId(contactId) 79 | } 80 | 81 | const { result } = await db.collection('contacts').deleteMany(contact) 82 | return result.n 83 | } 84 | 85 | // todo: 86 | async function replace (contact) {} 87 | 88 | // todo: 89 | async function update (contact) {} 90 | 91 | function documentToContact ({ _id: contactId, ...doc }) { 92 | return makeContact({ contactId, ...doc }) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/contacts/contact.js: -------------------------------------------------------------------------------- 1 | import requiredParam from '../helpers/required-param' 2 | import { InvalidPropertyError } from '../helpers/errors' 3 | import isValidEmail from '../helpers/is-valid-email.js' 4 | import upperFirst from '../helpers/upper-first' 5 | 6 | export default function makeContact ( 7 | contactInfo = requiredParam('contactInfo') 8 | ) { 9 | const validContact = validate(contactInfo) 10 | const normalContact = normalize(validContact) 11 | return Object.freeze(normalContact) 12 | 13 | function validate ({ 14 | firstName = requiredParam('firstName'), 15 | lastName = requiredParam('lastName'), 16 | emailAddress = requiredParam('emailAddress'), 17 | ...otherInfo 18 | } = {}) { 19 | validateName('first', firstName) 20 | validateName('last', lastName) 21 | validateEmail(emailAddress) 22 | return { firstName, lastName, emailAddress, ...otherInfo } 23 | } 24 | 25 | function validateName (label, name) { 26 | if (name.length < 2) { 27 | throw new InvalidPropertyError( 28 | `A contact's ${label} name must be at least 2 characters long.` 29 | ) 30 | } 31 | } 32 | 33 | function validateEmail (emailAddress) { 34 | if (!isValidEmail(emailAddress)) { 35 | throw new InvalidPropertyError('Invalid contact email address.') 36 | } 37 | } 38 | 39 | function normalize ({ emailAddress, firstName, lastName, ...otherInfo }) { 40 | return { 41 | ...otherInfo, 42 | firstName: upperFirst(firstName), 43 | lastName: upperFirst(lastName), 44 | emailAddress: emailAddress.toLowerCase() 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/contacts/contacts-endpoint.js: -------------------------------------------------------------------------------- 1 | import { 2 | UniqueConstraintError, 3 | InvalidPropertyError, 4 | RequiredParameterError 5 | } from '../helpers/errors' 6 | import makeHttpError from '../helpers/http-error' 7 | import makeContact from './contact' 8 | 9 | export default function makeContactsEndpointHandler ({ contactList }) { 10 | return async function handle (httpRequest) { 11 | switch (httpRequest.method) { 12 | case 'POST': 13 | return postContact(httpRequest) 14 | 15 | case 'GET': 16 | return getContacts(httpRequest) 17 | 18 | default: 19 | return makeHttpError({ 20 | statusCode: 405, 21 | errorMessage: `${httpRequest.method} method not allowed.` 22 | }) 23 | } 24 | } 25 | 26 | async function getContacts (httpRequest) { 27 | const { id } = httpRequest.pathParams || {} 28 | const { max, before, after } = httpRequest.queryParams || {} 29 | 30 | const result = id 31 | ? await contactList.findById({ contactId: id }) 32 | : await contactList.getItems({ max, before, after }) 33 | return { 34 | headers: { 35 | 'Content-Type': 'application/json' 36 | }, 37 | statusCode: 200, 38 | data: JSON.stringify(result) 39 | } 40 | } 41 | 42 | async function postContact (httpRequest) { 43 | let contactInfo = httpRequest.body 44 | if (!contactInfo) { 45 | return makeHttpError({ 46 | statusCode: 400, 47 | errorMessage: 'Bad request. No POST body.' 48 | }) 49 | } 50 | 51 | if (typeof httpRequest.body === 'string') { 52 | try { 53 | contactInfo = JSON.parse(contactInfo) 54 | } catch { 55 | return makeHttpError({ 56 | statusCode: 400, 57 | errorMessage: 'Bad request. POST body must be valid JSON.' 58 | }) 59 | } 60 | } 61 | 62 | try { 63 | const contact = makeContact(contactInfo) 64 | const result = await contactList.add(contact) 65 | return { 66 | headers: { 67 | 'Content-Type': 'application/json' 68 | }, 69 | statusCode: 201, 70 | data: JSON.stringify(result) 71 | } 72 | } catch (e) { 73 | return makeHttpError({ 74 | errorMessage: e.message, 75 | statusCode: 76 | e instanceof UniqueConstraintError 77 | ? 409 78 | : e instanceof InvalidPropertyError || 79 | e instanceof RequiredParameterError 80 | ? 400 81 | : 500 82 | }) 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/contacts/contacts-endpoint.spec.js: -------------------------------------------------------------------------------- 1 | import handle from '.' 2 | 3 | describe('Contacts Endpoint', () => { 4 | it('Will NOT create a contact without an email address', async () => { 5 | const result = await handle({ 6 | method: 'POST', 7 | body: JSON.stringify({ 8 | firstName: 'lksdjf', 9 | lastName: 'dkhflkjhf' 10 | }) 11 | }) 12 | expect(result).toEqual({ 13 | headers: { 14 | 'Content-Type': 'application/json' 15 | }, 16 | statusCode: 400, 17 | data: JSON.stringify({ 18 | success: false, 19 | error: 'emailAddress can not be null or underfined.' 20 | }) 21 | }) 22 | }) 23 | it('Will NOT create a contact with an invalid email address', async () => { 24 | const result = await handle({ 25 | method: 'POST', 26 | body: JSON.stringify({ 27 | firstName: 'lksdjf', 28 | lastName: 'dkhflkjhf', 29 | emailAddress: 'not-valid-at-all' 30 | }) 31 | }) 32 | expect(result).toEqual({ 33 | headers: { 34 | 'Content-Type': 'application/json' 35 | }, 36 | statusCode: 400, 37 | data: JSON.stringify({ 38 | success: false, 39 | error: 'Invalid contact email address.' 40 | }) 41 | }) 42 | }) 43 | it('Will NOT create a contact without a first name', async () => { 44 | const result = await handle({ 45 | method: 'POST', 46 | body: JSON.stringify({ 47 | emailAddress: 'bill@devmastery.com', 48 | lastName: 'dkhflkjhf' 49 | }) 50 | }) 51 | expect(result).toEqual({ 52 | headers: { 53 | 'Content-Type': 'application/json' 54 | }, 55 | statusCode: 400, 56 | data: JSON.stringify({ 57 | success: false, 58 | error: 'firstName can not be null or underfined.' 59 | }) 60 | }) 61 | }) 62 | it('Will NOT create an invalid first name', async () => { 63 | const result = await handle({ 64 | method: 'POST', 65 | body: JSON.stringify({ 66 | firstName: 'a', 67 | emailAddress: 'bill@devmastery.com', 68 | lastName: 'dkhflkjhf' 69 | }) 70 | }) 71 | expect(result).toEqual({ 72 | headers: { 73 | 'Content-Type': 'application/json' 74 | }, 75 | statusCode: 400, 76 | data: JSON.stringify({ 77 | success: false, 78 | error: `A contact's first name must be at least 2 characters long.` 79 | }) 80 | }) 81 | }) 82 | 83 | it('Will NOT create a contact without a valid last name', async () => { 84 | const result = await handle({ 85 | method: 'POST', 86 | body: JSON.stringify({ 87 | emailAddress: 'bill@devmastery.com', 88 | firstName: 'dkhflkjhf' 89 | }) 90 | }) 91 | expect(result).toEqual({ 92 | headers: { 93 | 'Content-Type': 'application/json' 94 | }, 95 | statusCode: 400, 96 | data: JSON.stringify({ 97 | success: false, 98 | error: 'lastName can not be null or underfined.' 99 | }) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/contacts/index.js: -------------------------------------------------------------------------------- 1 | import makeDb from '../db' 2 | import makeContactList from './contact-list' 3 | import makeContactsEndpointHandler from './contacts-endpoint' 4 | 5 | const database = makeDb() 6 | const contactList = makeContactList({ database }) 7 | const contactsEndpointHandler = makeContactsEndpointHandler({ contactList }) 8 | 9 | export default contactsEndpointHandler 10 | -------------------------------------------------------------------------------- /src/db/index.js: -------------------------------------------------------------------------------- 1 | import mongodb from 'mongodb' 2 | 3 | export default async function makeDb () { 4 | const MongoClient = mongodb.MongoClient 5 | const url = 'mongodb://localhost:27017' 6 | const dbName = 'mm_api_demo' 7 | const client = new MongoClient(url, { useNewUrlParser: true }) 8 | await client.connect() 9 | const db = await client.db(dbName) 10 | db.makeId = makeIdFromString 11 | return db 12 | } 13 | function makeIdFromString (id) { 14 | return new mongodb.ObjectID(id) 15 | } 16 | -------------------------------------------------------------------------------- /src/helpers/adapt-request.js: -------------------------------------------------------------------------------- 1 | export default function adaptRequest (req = {}) { 2 | return Object.freeze({ 3 | path: req.path, 4 | method: req.method, 5 | pathParams: req.params, 6 | queryParams: req.query, 7 | body: req.body 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /src/helpers/errors.js: -------------------------------------------------------------------------------- 1 | export class UniqueConstraintError extends Error { 2 | constructor (value) { 3 | super(`${value} must be unique.`) 4 | 5 | if (Error.captureStackTrace) { 6 | Error.captureStackTrace(this, UniqueConstraintError) 7 | } 8 | } 9 | } 10 | 11 | export class InvalidPropertyError extends Error { 12 | constructor (msg) { 13 | super(msg) 14 | 15 | if (Error.captureStackTrace) { 16 | Error.captureStackTrace(this, InvalidPropertyError) 17 | } 18 | } 19 | } 20 | 21 | export class RequiredParameterError extends Error { 22 | constructor (param) { 23 | super(`${param} can not be null or undefined.`) 24 | 25 | if (Error.captureStackTrace) { 26 | Error.captureStackTrace(this, RequiredParameterError) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/helpers/http-error.js: -------------------------------------------------------------------------------- 1 | export default function makeHttpError ({ statusCode, errorMessage }) { 2 | return { 3 | headers: { 4 | 'Content-Type': 'application/json' 5 | }, 6 | statusCode, 7 | data: JSON.stringify({ 8 | success: false, 9 | error: errorMessage 10 | }) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/helpers/is-valid-email.js: -------------------------------------------------------------------------------- 1 | export default function isValidEmail (email) { 2 | const valid = new RegExp(/^[^@\s]+@[^@\s]+\.[^@\s]+$/) 3 | return valid.test(email) 4 | } 5 | -------------------------------------------------------------------------------- /src/helpers/required-param.js: -------------------------------------------------------------------------------- 1 | import { RequiredParameterError } from './errors' 2 | 3 | export default function requiredParam (param) { 4 | throw new RequiredParameterError(param) 5 | } 6 | -------------------------------------------------------------------------------- /src/helpers/upper-first.js: -------------------------------------------------------------------------------- 1 | export default function upperFirst (word) { 2 | if (word.length === 1) { 3 | return word.toUpperCase() 4 | } 5 | return word.charAt(0).toUpperCase() + word.substring(1) 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import bodyParser from 'body-parser' 3 | import handleContactsRequest from './contacts' 4 | import adaptRequest from './helpers/adapt-request' 5 | 6 | const app = express() 7 | app.use(bodyParser.json()) 8 | 9 | app.all('/contacts', contactsController) 10 | app.get('/contacts/:id', contactsController) 11 | 12 | function contactsController (req, res) { 13 | const httpRequest = adaptRequest(req) 14 | handleContactsRequest(httpRequest) 15 | .then(({ headers, statusCode, data }) => 16 | res 17 | .set(headers) 18 | .status(statusCode) 19 | .send(data) 20 | ) 21 | .catch(e => res.status(500).end()) 22 | } 23 | 24 | app.listen(9090, () => console.log(`Listening on port 9090`)) 25 | --------------------------------------------------------------------------------