RESTful Node.js: A Structured Approach
  • Book Cover
  • About the Author
  • Links and Resources
  • Part I: The Why
    • Foreword
    • Preface
    • Chapter 1: Introduction
      • The Rise of REST and Distributed Systems
      • Problem #1: Structureless Design, Structureless REST
      • The Emergence of JavaScript and Node.js
      • Problem #2: Structureless JavaScript, Structureless Node.js
      • Behold, the Solution: A Structured Approach
      • Summary
  • Part 2: The Theory
    • Chapter 2: REST Origins
      • A Brief History of the Web and the Birth of REST
      • REST vs. HTTP
      • REST - The Abstract Web Architecture
      • HTTP - A Peak at REST's Concrete Implementation
      • What does it mean for an API to be RESTful?
      • Measuring "RESTfulness" with Richardson Maturity Model
      • Pragmatic REST vs Dogmatic REST
      • Summary
    • Chapter 3: RESTful API Design Guidelines and "Best Practices"
      • Theories vs. Principles vs. Guidelines
      • URI Design
      • Method Verbs
      • Status Codes
      • Representational Design
      • Metadata Design
      • Versioning Strategies
      • Security Considerations
      • Documentation
      • Case Study: GitHub
      • Summary
    • Chapter 4: Structured JavaScript Architecture
      • The Monstrous Monolith and Its Downfall
      • Layered/N-Tier Architecture: The Unpopular Proven Way
      • Microservices and Distributed Computing: A Popular Misdirection
      • Summary
    • Chapter 5: The 8 Step Recipe
      • Route Name (URI)
      • Input Request
      • Middleware
      • Validation
      • Domain
      • Events
      • Output Response
      • Test, Refactor, Document
      • Summary
  • Part 3: The Code
    • Chapter 6: Introduction to the Bookstore API
      • The Bookstore API Endpoint Specifications
      • API Design and Code Structure
      • Project Setup
      • Summary
    • Chapter 7: Retrieving Books from our API
      • Retrieving All Books - Planning
      • Retrieving All Books - Implementation
      • Retrieving A Book By ID - Planning
      • Retrieving A Book By ID - Implementation
      • Summary
    • Chapter 8: Adding Authentication to our API
      • Registering the User - Planning
      • Registering the User - Implementation
      • Logging the User In - Planning
      • Logging the User In - Implementation
      • Getting Authenticated User - Planning
      • Getting Authenticated User - Implementation
      • Summary
    • Chapter 9: Adding the Create, Update, and Delete Operations to our API
      • Creating A Book Listing - Planning
      • Creating A Book Listing - Implementation
      • Updating A Book Listing By ID - Planning
      • Updating A Book Listing By ID - Implementation
      • Deleting A Book Listing By ID - Planning
      • Deleting A Book Listing By ID - Implementation
      • Summary
    • Chapter 10: Testing our API
      • Testing the Request
      • Testing the Middleware
      • Testing the Validation
      • Testing the Domain
      • Testing the Event
      • Testing the Response
      • Testing the Controller
      • Integration Test
      • Summary
  • Conclusion
    • Final Words
  • Bonus!
    • Refactoring to HATEOAS
  • Appendix
    • Sources & References
Powered by GitBook
On this page
  • 1 - Route Name
  • 2 - Input Request
  • 3 - Middleware
  • 4 - Validation
  • 5 - Domain
  • 6 - Events
  • 7 - Response
Edit on GitHub
  1. Part 3: The Code
  2. Chapter 9: Adding the Create, Update, and Delete Operations to our API

Updating A Book Listing By ID - Implementation

1 - Route Name

PUT /api/v1/books/:id

Just like we planned, let's add in the route.

File: src/routes/book.route.js

const express = require('express')
const router = express.Router()

const {
  getAllBooks,
  getBookById,
  createABook,
  updateABook
} = require('../controllers/book')

router.get('/', getAllBooks)
router.get('/:id', getBookById)
router.post('/', createABook)

// This is the new route we are adding in
router.put('/:id', updateABook)

Let's also create our controller so we can fill in the details later on.

File: src/controllers/book/updateABook.js

const catchException = require('../../utils/catchExceptions')

/**
 * Updates an existing book listing by id.
 */
const updateABook = catchException(async (req, res) => {
  // we'll fill in the details after we get each of the other layers ready
})

module.exports = updateABook

2 - Input Request

For the input request, this will seem very familiar. It's because this is the exact code we used for our createBookRequestDto.js with the exception of an additional id field.

File: src/controllers/requests/updateBookRequestDto.js

const ApiException = require('../utils/ApiException')

const fields = [
  'id',
  'title',
  'description',
  'price',
  'author',
  'datePublished'
]

const updateBookRequestDto = (data) => {
  const errors = []
  fields.forEach((field) => {
    if (!(field in data)) {
      errors.push(`This DTO's property is required: ${field}.`)
    }
  })

  if (errors.length > 0) {
    throw new ApiException({
      status: 'error',
      code: 422,
      message: 'Input fields are of not the correct form.',
      data: null,
      errors
    })
  }

  return data
}

module.exports = updateBookRequestDto

3 - Middleware

File: src/routes/book.route.js

// We'll be adding in the isAuthenticated middleware and also our
// new middleware, the bookPermission middleware
router.put('/:id', isAuthenticated, bookPermission, updateABook)

As stated previously, we need a way to check if the request book to be updated does indeed belong to the currently authenticated user.

This will help us do so. Notice also that we are going to be using the bookService.getBookId method we created previously. This is a great functionality to reuse because the getBookById inherently has a check to see if the bookId is correct, otherwise it will throw an exception.

File: src/middleware/bookPermission.middleware.js

const globalResponseDto = require('../responses/globalResponseDto')
const catchExceptions = require('../utils/catchExceptions')
const bookService = require('../domain/services/book.service')

const bookPermission = catchExceptions(async (req, res, next) => {
  // When updating or deleting a book, the book must belong to the user that created it
  const bookId = req.params.id
  const book = await bookService.getBookById(bookId)

  // check to see if the current authenticated user's the owner of the
  // requested bookId
  if (req.session.user._id !== book.userId.toString()) {
    res.status(401).json(
      globalResponseDto({
        status: 'error',
        code: 401,
        message:
          'Access denied: you must be the owner of this book when updating or deleting it.',
        data: null,
        errors: [
          'Access denied: you must be the owner of this book when updating or deleting it.'
        ]
      })
    )
  }

  next()
})

module.exports = bookPermission

4 - Validation

Again, this is very similar to the createBookValidator with the exception of an additional id field. You are probably wondering why we require all fields instead of a subset of them. Do recall that this is a PUT request, which means we must accept the entire resource into our API as we are re-updating the entire entity in database.

File: src/validators/updateBookValidator.js

const Validator = require('validatorjs')
const ApiException = require('../utils/ApiException')

/**
 * @param {*} data {
 *  - id
 *  - title
 *  - description
 *  - price
 *  - author
 *  - datePublished
 * }
 *
 * @returns Validator
 */
const updateBookValidator = (data) => {
  const rules = {
    title: 'required',
    description: 'required',
    price: 'required|numeric|min:1',
    author: 'required',
    datePublished: 'required'
  }

  const validator = new Validator(data, rules)

  if (validator.fails()) {
    let errors = []
    for (const field in validator.errors.errors) {
      errors = errors.concat(validator.errors.errors[field])
    }

    throw new ApiException({
      message: 'There were errors with the validation',
      status: 'error',
      code: 400,
      data: null,
      errors
    })
  }

  return validator
}

module.exports = updateBookValidator

5 - Domain

Yet again, this is similar to what we had before when we wrote the create method in our bookRepository and the createBook method in our bookService. We now do this for updating a book.

File: src/domain/repositories/book.repository.js

const Model = require('../models/book.model')

// Update
const updateById = async (id, book) => {
  const updatedBook = await Model.findByIdAndUpdate(id, book)

  return updatedBook
}

File: src/domain/services/book.service.js

const bookRepository = require('../repositories/book.repository')

// Update a book
const updateBookById = async (book) => {
  const newlyUpdatedBook = await bookRepository.updateById(book.id, book)

  return newlyUpdatedBook
}

6 - Events

None.

7 - Response

Now for the response.

Do note that our isAuthenticated and bookPermission middleware will handle most of the error messages for us.

For our success message we will put have the following in our controller.

File: src/controllers/book/updateABook.js

const catchException = require('../../utils/catchExceptions')
const globalResponseDto = require('../../responses/globalResponseDto')

const updateBookRequestDto = require('../../requests/updateBookRequestDto')
const updateBookValidator = require('../../validators/updateBookValidator')
const bookService = require('../../domain/services/book.service')
const bookResponseDto = require('../../responses/bookResponseDto')

/**
 * Updates an existing book listing by id.
 */
const updateABook = catchException(async (req, res) => {
  const updateBookFields = updateBookRequestDto({
    id: req.params.id,
    ...req.body
  })

  updateBookValidator(updateBookFields)

  const updatedBook = await bookService.updateBookById(updateBookFields)

  res.json(
    globalResponseDto({
      status: 'success',
      code: 200,
      message: `The book has successfully been updated.`,
      data: bookResponseDto(updatedBook),
      errors: null
    })
  )
})

module.exports = updateABook

Once again, this is nice and thin, and easy on the eyes.

PreviousUpdating A Book Listing By ID - PlanningNextDeleting A Book Listing By ID - Planning

Last updated 3 years ago