Creating A Book Listing - Implementation

1 - Route Name

POST /api/v1/books

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 } = require('../controllers/book')

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

// This is the new route we are adding in
router.post('/', createABook)

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

File: src/controllers/book/createABook.js

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

/**
 * Creates a new book listing.
 */
const createABook = catchException(async (req, res) => {
  // we'll fill in the details after we get each of the other layers ready
})

module.exports = createABook

2 - Input Request

Very similar to how we implemented our registerUserRequestDto, we'll create a createBookRequestDto that has the fields title, description, price, author, and datePublished in it.

File: src/requests/createBookRequestDto.js

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

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

const createBookRequestDto = (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 = createBookRequestDto

3 - Middleware

We are going to be reusing the isAuthenticated middleware from before and add it in right before our controller.

File: src/routes/book.route.js

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

const { createABook } = require('../controllers/book')

const isAuthenticated = require('../middleware/auth.middleware')

// The isAuthenticated middleware is going to protect this route
// from unauthenticated users from accessing it
router.post('/', isAuthenticated, createABook)

4 - Validation

Let's not forget about the validation layer. Again, this is very similar to the registerUserValiator function we created in the previous chapter except it's just going to be for different fields.

File: src/validators/createBookValidator.js

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

/**
 * @param {*} data {
 *  - title
 *  - description
 *  - price
 *  - author
 *  - datePublished
 * }
 *
 * @returns Validator
 */
const createBookValidator = (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 = createBookValidator

5 - Domain

As mentioned in the previous planning section, this will be the model we create.

File: src/domain/models/book.model.js

const mongoose = require('mongoose')
const Schema = mongoose.Schema

const bookModel = new mongoose.Schema({
  userId: {
    type: Schema.Types.ObjectId,
    ref: 'user'
  },
  title: {
    type: String,
    required: true
  },
  description: {
    type: String
  },
  price: {
    type: Number,
    required: true
  },
  author: {
    type: String
  },
  datePublished: {
    type: String
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
})

module.exports = mongoose.model('book', bookModel)

This will be the repository layer on top of that model.

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

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

// Saves a book in the database
const create = async (newBook) => {
  const book = new Model(newBook)
  return await book.save()
}

module.exports = {
  create
}

Finally, our service layer that will use the bookRepository layer.

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

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

// Create a book
const createBook = async (book) => {
  return bookRepository.create(book)
}

module.exports = {
  createBook
}

6 - Events

None.

7 - Response

If we put this all together now in the controller, this is what we'll get.

const mongoose = require('mongoose')
const globalResponseDto = require('../../responses/globalResponseDto')
const createBookRequestDto = require('../../requests/createBookRequestDto')
const catchException = require('../../utils/catchExceptions')
const bookService = require('../../domain/services/book.service')
const bookResponseDto = require('../../responses/bookResponseDto')
const createBookValidator = require('../../validators/createBookValidator')

/**
 * Creates a new book listing.
 */
const createABook = catchException(async (req, res) => {
  const createBookRequest = createBookRequestDto({
    id: req.session.user.id,
    ...req.body
  })

  createBookValidator(createBookRequest)

  const book = await bookService.createBook({
    userId: mongoose.Types.ObjectId(req.session.user._id),
    ...req.body
  })

  res.status(200).json(
    globalResponseDto({
      status: 'success',
      code: 200,
      message: `Book has successfully been added to the database.`,
      data: bookResponseDto(book),
      errors: null
    })
  )
})

module.exports = createABook

The createBookRequestDto and the createBookValidator will naturally throw an ApiException which will yield the correct output response as we stated previously in the planning section of this endpoint.

In case of no errors and we get a successful pass, we will reuse our bookResponseDto from which we created previously on the output of the bookService.create method we just implemented and return the newly created book in our response.

Last updated