diff --git a/README.md b/README.md index c4fc73be..281223b5 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,9 @@ In this workshop we're going to look at how to use express with a postgres database. ## Learning Objectives -* Explain how REST API methods interact with a relational database -* Implement a REST API backed by a database using express and postgres + +- Explain how REST API methods interact with a relational database +- Implement a REST API backed by a database using express and postgres ## Setup @@ -20,16 +21,16 @@ For this exercise we will also need to configure our database: 6. Copy the SQL from the files in the `sql/` directory and run them in TablePlus -* create-books.sql -* create-pets.sql -* insert-books.sql -* insert-pets.sql +- create-books.sql +- create-pets.sql +- insert-books.sql +- insert-pets.sql 7. Copy the URL of your instance -8. Create a file `.env` in the __root directory__ of your project. It should be right next to the `.env.example` file. It should contain a single line, which contains the *environment variable* used to specify the url from the instance created above. See the example file for reference. +8. Create a file `.env` in the **root directory** of your project. It should be right next to the `.env.example` file. It should contain a single line, which contains the _environment variable_ used to specify the url from the instance created above. See the example file for reference. -7. Type `npm start`, which starts a development server that will reload whenever you make any changes to source files. +9. Type `npm start`, which starts a development server that will reload whenever you make any changes to source files. All being well, you will have a terminal window that looks like the following: @@ -38,60 +39,68 @@ All being well, you will have a terminal window that looks like the following: _Figure 2: The terminal window where the express server is running successfully_ ## Interacting with the Database + To interact with the database we will use the [node-postgres](https://node-postgres.com/) library. We will use the [query](https://node-postgres.com/features/queries) method to send SQL queries to the database sever and receive responses. The `db/index.js` file establishes the connection to the database. Your instructor will walk through this with you. ## Demo + Your instructor will demonstrate implementing some of the books API, now using a real database. You will complete the API spec implementation ## Instructions + - Implement the [API spec](https://boolean-uk.github.io/api-express-database/standard) ## Tests Run the following commands from your project directory to run the test suites: + ```sh -$ npm test # standard criteria -$ npm run test-extensions # extension criteria +npm test # standard criteria +npm run test-extensions # extension criteria ``` You can also focus on one test at a time - use the [jest docs](https://jestjs.io/docs/cli) to help filter which tests to run. We recommend you run tests manually with the option `--forceExit`. For example, for the following test: + ```js it("will list all books", async () => { - const response = await supertest(app).get("/books") + const response = await supertest(app).get("/books"); - expect(response.status).toEqual(200) - expect(response.body.books).not.toEqual(undefined) - expect(response.body.books.length).toEqual(2) - const expectedBooks = [book1, book2] + expect(response.status).toEqual(200); + expect(response.body.books).not.toEqual(undefined); + expect(response.body.books.length).toEqual(2); + const expectedBooks = [book1, book2]; response.body.books.forEach((retrievedBook, index) => { - expect(retrievedBook.title).toEqual(expectedBooks[index].title) - }) -}) + expect(retrievedBook.title).toEqual(expectedBooks[index].title); + }); +}); ``` Here are two ways to run it. + ```sh -$ npx jest -t "will list all books" --forceExit -$ npx jest test/api/routes/books.spec.js --forceExit # remember to add the 'f' before it() +npx jest -t "will list all books" --forceExit +npx jest test/api/routes/books.spec.js --forceExit # remember to add the 'f' before it() ``` ## Extension 1 + - Implement the [extension API spec](https://boolean-uk.github.io/api-express-database/extension) -- This API spec has some of the same endpoints as the Standard Criteria API spec, but they are **in addition to / build - on top of** that one. +- This API spec has some of the same endpoints as the Standard Criteria API spec, but they are **in addition to / build + on top of** that one. ## Extension 2 + So far we've been including all our database code directly in our route handlers. In a real application, this is considered bad practice. It would become difficult to maintain as the code base grows, and we are also mixing concerns. We have routing code, request/response handling and database access all in a single function. This leads to tight coupling and low cohesion. -It is better practice to split your code into different *layers*. This helps keep our code decoupled. Rather than your route handler implementing all your logic, you can introduce controller functions that handle your routes as well as specific functions for calling the database. There is no single "correct" approach, but here are some examples: +It is better practice to split your code into different _layers_. This helps keep our code decoupled. Rather than your route handler implementing all your logic, you can introduce controller functions that handle your routes as well as specific functions for calling the database. There is no single "correct" approach, but here are some examples: -* [MDN Express controllers and routes](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/routes) -* [Express REST API structure](https://www.coreycleary.me/project-structure-for-an-express-rest-api-when-there-is-no-standard-way) +- [MDN Express controllers and routes](https://developer.mozilla.org/en-US/docs/Learn/Server-side/Express_Nodejs/routes) +- [Express REST API structure](https://www.coreycleary.me/project-structure-for-an-express-rest-api-when-there-is-no-standard-way) There is also an example boolean repository that provides a suggested structure: -* [API Express Architecture](https://github.com/boolean-uk/api-express-architecture-example) +- [API Express Architecture](https://github.com/boolean-uk/api-express-architecture-example) Update your implementation to match the structure of the above repo. Controllers functions should handle your requests and responses, and repository functions should handle your database access. diff --git a/db/index.js b/db/index.js index af723442..be94a422 100644 --- a/db/index.js +++ b/db/index.js @@ -11,7 +11,7 @@ const client = { // on the contents of our env file // Create a new connection to the database using the Client // object provided by the postgres node module - const dbClient = new Client(process.env.PGURL) + const dbClient = new Client(process.env.DATABASE_URL) // connect a connection await dbClient.connect() // execute the query diff --git a/package-lock.json b/package-lock.json index 37ccdff8..f08df953 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1441,12 +1441,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1740,9 +1740,9 @@ "dev": true }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -2024,16 +2024,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -2064,43 +2064,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/express/node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/faker": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz", @@ -2128,9 +2091,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/src/data/books.js b/src/data/books.js new file mode 100644 index 00000000..e1f6ea27 --- /dev/null +++ b/src/data/books.js @@ -0,0 +1,59 @@ +const db = require("./db.js"); + + +const all = async () => { + const result = await db.query("SELECT * FROM books"); + return result.rows; +}; + +const getById = async (id) => { + const result = await db.query("SELECT * FROM books WHERE id = $1", [id]); + return result.rows[0]; +}; + +const create = async (book) => { + const result = await db.query( + "INSERT INTO books (title, type, author, topic, publication_date, pages) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *", + [ + book.title, + book.type, + book.author, + book.topic, + book.publication_date, + book.pages, + ] + ); + return result.rows[0]; +}; + +const update = async (id, updates) => { + const result = await db.query( + "UPDATE books SET title = $1, type = $2, author = $3, topic = $4, publication_date = $5, pages = $6 where id = $7 RETURNING *", + [ + updates.name, + updates.type, + updates.author, + updates.topic, + updates.publication_date, + updates.pages, + id, + ] + ); + return result.rows[0]; +}; + +const remove = async (id) => { + const result = await db.query( + "DELETE FROM books WHERE id = $1 RETURNING *", + [id] + ); + return result.rows[0]; +}; + +module.exports = { + all, + getById, + create, + update, + remove, +}; diff --git a/src/data/db.js b/src/data/db.js new file mode 100644 index 00000000..308f4880 --- /dev/null +++ b/src/data/db.js @@ -0,0 +1,23 @@ +const { Client } = require("pg"); +require('dotenv').config(); + +const { HOST, PORT, DATABASE, USERNAME, PASSWORD, DATABASE_URL } = process.env; + +//const db = new Pool({ + //host: PGHOST, + //database: PGDATABASE, + //username: PGUSER, + //password: PGPASSWORD, +//}); + +const db = new Client(DATABASE_URL); + +const init = async () => { + await db.connect(); +}; +init() + + + + +module.exports = db; diff --git a/src/data/pets.js b/src/data/pets.js new file mode 100644 index 00000000..4f454b9d --- /dev/null +++ b/src/data/pets.js @@ -0,0 +1,49 @@ +const db = require("./db.js"); + +const all = async () => { + const result = await db.query("SELECT * FROM pets"); + return result.rows; +}; + +const getById = async (id) => { + const result = await db.query("SELECT * FROM pets WHERE id = $1", [id]); + return result.rows[0]; +}; + +const create = async (pet) => { + const result = await db.query( + "INSERT INTO pets (name, age, type, breed, has_microchip) VALUES ($1, $2, $3, $4, $5) RETURNING *", + [pet.name, pet.age, pet.type, pet.breed, pet.has_microchip] + ); + return result.rows[0]; +}; + +const update = async (id, updates) => { + const result = await db.query( + "UPDATE pets SET name = $1, age = $2, type = $3, breed = $4, has_microchip = $5 where id = $6 RETURNING *", + [ + updates.name, + updates.age, + updates.type, + updates.breed, + updates.has_microchip, + id, + ] + ); + return result.rows[0]; +}; + +const remove = async (id) => { + const result = await db.query("DELETE FROM pets WHERE id = $1 RETURNING *", [ + id, + ]); + return result.rows[0]; +}; + +module.exports = { + all, + getById, + create, + update, + remove, +}; diff --git a/src/routers/books.js b/src/routers/books.js index 1551dd87..6d38371c 100644 --- a/src/routers/books.js +++ b/src/routers/books.js @@ -1,9 +1,46 @@ -const express = require('express') -const router = express.Router() -const db = require("../../db"); +const express = require("express"); +const router = express.Router(); +const db = require("../data/books.js"); -router.get('/', async (req, res) => { +router.get("/", async (req, res) => { + const books = await db.all(); + res.status(200).json({ books: books }); +}); -}) +router.get("/:id", async (req, res) => { + const id = Number(req.params.id); + const book = await db.getById(id); + res.status(200).json({ book: book }); +}); -module.exports = router +router.post("/", async (req, res) => { + const newBook = req.body; + if (!newBook.title || !newBook.author || !newBook.type) { + res.status(400).send({ error: `Missing fields in request body` }); + return; + } + + const book = await db.create(newBook); + res.status(201).json({ book: book }); +}); + +router.put("/:id", async (req, res) => { + const id = Number(req.params.id); + const updates = req.body; + + if (!updates.title || !updates.author || !updates.type) { + res.status(400).send({ error: `Missing fields in request body` }); + return; + } + + const updated = db.update(updates); + res.status(201).json({ book: updated }); +}); + +router.delete("/:id", async (req, res) => { + const id = Number(req.params.id); + const result = await db.remove(id); + res.status(201).json({ book: result }); +}); + +module.exports = router; diff --git a/src/routers/pets.js b/src/routers/pets.js new file mode 100644 index 00000000..23a76232 --- /dev/null +++ b/src/routers/pets.js @@ -0,0 +1,58 @@ +const express = require("express"); +const router = express.Router(); +const db = require("../data/pets.js"); + +router.get("/", async (req, res) => { + const pets = await db.all(); + res.status(200).json({ pets: pets }); +}); + +router.get("/:id", async (req, res) => { + const id = Number(req.params.id); + const pet = await db.getById(id); + res.status(200).json({ pet: pet }); +}); + +router.post("/", async (req, res) => { + const newPet = req.body; + if ( + !newPet.name || + !newPet.age || + !newPet.type || + !newPet.breed || + !newPet.has_microchip + ) { + res.status(400).send({ error: `Missing fields in request body` }); + return; + } + + const pet = await db.create(newPet); + res.status(201).json({ pet: pet }); +}); + +router.put("/:id", async (req, res) => { + const id = Number(req.params.id); + const updates = req.body; + + if ( + !updates.name || + !updates.age || + !updates.type || + !updates.breed || + !updates.has_microchip + ) { + res.status(400).send({ error: `Missing fields in request body` }); + return; + } + + const updated = db.update(updates); + res.status(201).json({ pet: updated }); +}); + +router.delete("/:id", async (req, res) => { + const id = Number(req.params.id); + const result = await db.remove(id); + res.status(201).json({ pet: result }); +}); + +module.exports = router; diff --git a/src/server.js b/src/server.js index dac55e5d..233ceb88 100644 --- a/src/server.js +++ b/src/server.js @@ -10,7 +10,9 @@ app.use(express.json()); //TODO: Implement books and pets APIs using Express Modular Routers const booksRouter = require('./routers/books.js') +const petsRouter = require('./routers/pets.js') app.use('/books', booksRouter) +app.use('/pets', petsRouter) module.exports = app