en Documentación

JSDoc series III – Documentación Avanzada

En esta tercera y última entrada de la serie JSDoc se verán algunos temas avanzados como documentación de un API, plugins y templates para estilizar la página web.

Plugins y templates

Los plugins son extensiones que permiten aumentar la capacidad de documentación, al proveer más funcionalidades y propiedades que brindan un mayor nivel de detalle sobre la documentación.

En este ejemplo se trabajará en específico con el plugin de jsdoc-http-plugin, el cual provee varias propiedades y tipados apropiados para documentar una API.

La siguiente imagen muestra el archivo de configuración para JSDoc.

El plugin se coloca en la propiedad de «plugins» del archivo de configuración, además de ese plugins se utiliza otro por defecto el cual se llama plugins/markdown.

Además de los plugins, dentro de la propiedad «opts» se define la subpropiedad «template«, la cual permite estilizar la página web de la docuementación con algún template definido, algunos templates puede ser encontrado aquí.

Una vez seleccionado un template, solo basta con instalarlo en el proyecto de interés y configurarlo en archivo de configuración de JSDoc, tal y como se vé en la imagen anterior.

Para este ejemplo se utilizó la platilla de docdash, la cual puede ser instala con el siguiente comando.

npm i docdash -D

Esto instalará la platilla dentro del directorio de node_modules, al configurarlo una vez que se realice la documentación aparecerá la página web estilizada.

Documentando un API

Una de las documentaciones más típicas de realizar en el desarrollo de un servidor son las APIs, las cuales son un conjunto de rutas y controles que realizan alguna determinada función.

A este conjunto de rutas y controles se les conoce como endpoints y sobre estos se necesita precisar en detalle, dado que establecen la manera en que el servidor se comunica para realizar las tareas solitadas.

El siguiente código muestra las rutas para un servidor que maneja un CRUD de todos (tareas por realizar).

/**
 * Routers
 * @module todo-routers
 */

const { check } = require('express-validator')

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

const todoController = require('../controllers/todo-controller')
const { isAuthenticated } = require('../helpers/auth')

/**
 * Get all the todos objects
 * @name getTodos
 * @path {GET} /
 * @param {function} todoController.getTodos - A controller callback
 */

router.get('/', isAuthenticated, todoController.getTodos)

/**
 * Get one todo object
 * @name getTodo
 * @path {GET} /:id
 * @param {string} :id Is a mongoDB unique identifier
 * @param {function} todoController.getTodo - A controller callback
 */

router.get('/:id', isAuthenticated, [
  check('id').notEmpty().withMessage('id parameter can not be empty.')
    .isMongoId().withMessage('id parameters must be a mongo ID.')
], todoController.getTodo)

/**
 * Create a todo object
 * @name postTodo
 * @path {POST} /add
 * @param {function} todoController.createTodo - A controller callback
 */

router.post('/add', isAuthenticated, [
  check('todo_description').notEmpty()
    .withMessage('Description can not be empty.')
    .isString().withMessage('Description must be a string type.'),
  check('todo_responsible').notEmpty()
    .withMessage('Responsible can not be empty.')
    .isString().withMessage('Responsible must be a string type.'),
  check('todo_priority').notEmpty().withMessage('Priority can not be empty.')
    .isString().withMessage('Priority must be a string type.'),
  check('todo_completed').notEmpty().withMessage('Completed can not be empty.')
    .isBoolean().withMessage('Completed must be a boolean type.')
], todoController.createTodo)

/**
 * Update a todo object
 * @name updateTodo
 * @path {POST} /update/:id
 * @param {string} :id Is a mongoDB unique identifier
 * @param {function} todoController.updateTodo - A controller callback
 */

router.post('/update/:id', isAuthenticated, [
  check('id').notEmpty().withMessage('id parameter can not be empty')
    .isMongoId().withMessage('id parameters must be a mongo ID'),
  check('todo_description').notEmpty()
    .withMessage('Description can not be empty.')
    .isString().withMessage('Description must be a string type.'),
  check('todo_responsible').notEmpty()
    .withMessage('Responsible can not be empty.')
    .isString().withMessage('Responsible must be a string type.'),
  check('todo_priority').notEmpty().withMessage('Priority can not be empty.')
    .isString().withMessage('Priority must be a string type.'),
  check('todo_completed').notEmpty().withMessage('Completed can not be empty.')
    .isBoolean().withMessage('Completed must be a boolean type.')
], todoController.updateTodo)

/**
 * Delete a todo object
 * @name deleteTodo
 * @path {GET} /delete/:id
 * @param {string} :id Is a mongoDB unique identifier
 * @param {function} todoController.deleteTodo - A controller callback
 */

router.get('/delete/:id', isAuthenticated, [
  check('id').notEmpty().withMessage('id parameter can not be empty')
    .isMongoId().withMessage('id parameters must be a mongo ID')
], todoController.deleteTodo)

module.exports = router

El primer keyword de tipado que aparece por parte del plugin jsdoc-http-plugin es:

  • @path {METHOD} – path: Esta propiedad define la ruta de un endpoint, se define el método de http que utiliza y seguidamente se brinda la ruta.
  • Los demás keyword pertenencen a la documentación básica de JSDoc, los cuales fueron analizados en la entrega anterior.

A continuación el siguiente código muestra los controles o lógica de los endpoints:

/**
 * Controllers
 * @module todo-controller
 */

/**
  * errors
  * @typedef {Array<object>} errors
  * @property {any} value - A value sended on body property
  * @property {string} msg - A message explaining why the request failure
  * @property {string} param - The property where the error is
  * @property {string} location - The site where the property is
  */

/**
   * message
   * @typedef {object} message
   * @property {string} msg -  A message from server
   */

const { validationResult } = require('express-validator')

const Todo = require('../models/todo')

/**
 * Get all the todos from DB
 * @function
 * @name getTodos
 * @param {object} res - A object promise, will be a Todo's array
 * on success or string on error.
 * @response {object} res - A Todos array object
 * or void if error
 */

exports.getTodos = async (req, res) => {
  const todos = await Todo.find({ todo_user: req.user.id })
    .catch(err => {
      console.error(err.message)
    })
  res.json(todos)
}

/**
 * Get one Todo from DB
 * @function
 * @name getTodo
 * @query {any} req - Todo id is type MongoId
 * @param {object} res - A object promise, will be a Todo object
 * on code 200, a errors object array on code 400 and string message on code 500
 * @code {200} If the Todo is found
 * @response {Todo} res - A Todo object
 * @code {400} If the req have errors (bad request)
 * @respose {errors} errors - A errors object array
 * @code {500} If an internal error happen
 * @response {string} res - A error message if an error happens on server
 */

exports.getTodo = async (req, res) => {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() })
  }
  const id = req.params.id
  const todo = await Todo.findById(id).catch(err => {
    console.error(err.message)
    res.status(500).send('getting todo failed')
  })
  res.json(todo)
}

/**
 * Create a todo object on DB
 * @function
 * @name createTodo
 * @param {Todo} req - Todo object
 * @body {string} todo_description - A todo description
 * @body {string} todo_responsible - Name of todo's responsible
 * @body {string} todo_priority - A todo priority level
 * @body {boolean} todo_completed - Todo completed indicator
 * @param {object} res - A object promise, will be a Todo's object array
 * on code 200, a errors object array on code 400 and string message on code 500
 * @code {200} If the object is created
 * @response {object} res - A success message object
 * @code {400} If the req have errors (bad request)
 * @respose {errors} res - A errors object array
 * @code {500} If an internal error happen
 * @response {message} res - A error message if an error happens on server
 */

exports.createTodo = async (req, res, next) => {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() })
  }
  const todo = new Todo(req.body)
  todo.todo_user = req.user.id
  await todo.save().then(message => {
    res.status(200).json({ msg: 'todo added successfully' })
  })
    .catch(err => {
      console.error(err.message)
      next(err)
    })
}

/**
 * Update a todo object on DB
 * @function
 * @name updateTodo
 * @query {any} req - Todo id is type MongoId
 * @body {string} todo_description - A todo description
 * @body {string} todo_responsible - Name of todo's responsible
 * @body {string} todo_priority - A todo priority level
 * @body {boolean} todo_completed - Todo completed indicator
 * @param {object} res - A object promise, will be a message object
 * on code 200, a errors object array on code 400 and string message
 * on code 500
 * @code {200} If the object is update
 * @response {message} res - A success message object
 * @code {400} If the req have errors (bad request)
 * @respose {errors} res - A errors object array
 * @code {500} If an internal error happen
 * @response {string} res - A error message if an error happens on server
 * @code {404} If the todo is not found
 * @respose {message} res - A error message
 */

exports.updateTodo = async (req, res) => {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() })
  }
  await Todo.findById(req.params.id, function (err, todo) {
    if (err) {
      console.error(err.message)
    }
    if (!todo) {
      res.status(404).json({ msg: 'Data is not found' })
    } else {
      const keys = ['todo_description', 'todo_responsible', 'todo_priority',
        'todo_completed']
      keys.forEach(key => { todo[key] = req.body[key] })

      todo.save().then(todo => {
        res.json({ msg: 'Todo update' })
      })
        .catch(err => {
          console.error(err.message)
          res.status(500).json({ msg: 'Update not possible' })
        })
    }
  })
}

/**
 * Delete one Todo from DB
 * @function
 * @name deleteTodo
 * @query {any} req - Todo id is type MongoId
 * @param {object} res - A object promise, will be a message object,
 * success on code 200 or error on code 500, an errors object array on code 400
 * @code {200} If the Todo is deleted
 * @response {message} res - A success message object
 * @code {400} If the req have errors (bad request)
 * @respose {errors} errors - A errors object array
 * @code {500} if an internal error happen
 * @response {mesaage} res - A error message object
 */

exports.deleteTodo = (req, res) => {
  const errors = validationResult(req)
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() })
  }
  Todo.findByIdAndRemove({ _id: req.params.id }, (err) => {
    if (err) {
      res.status(500).json(
        { msg: 'Error while finding and removing the element' })
    } else {
      res.json({ msg: 'Remove successfully' })
    }
  })
}

Los keywords por parte del plugin son los siguientes:

  • @query {type} query_name – description: Con está propiedad se decribe un query, dandole el tipo de dato, el nombre del query una descripción.
  • @code {code_number} description: Esta propiedad permite precisar el código de estado de una petición.
  • @response {response} – description: Con esta propiedad se describe el response que tendrá un determinado endpoint para cierto caso, necesita el response que va dar y una descripción.
  • @body {property_type} property_name – description: Finalmente esta propiedad permite describir con mayor detalle el body de una solicitud, muy útil cuando se trabaja con objetos y JSON. Necesita que se defina el tipo de dato de la propiedad del objeto, el nombre de dicha propiedad y una descripción.

Al realizar la documentación, el resultado se visualiza de la siguiente manera:

Como es de notar, el estilo de la página web cambió con respecto a de las entregas pasadas, además de que se vé el nivel de detalle que tiene la documentaación del endpoint createTodo, esto para el caso del controlador.

Para la ruta del endpoint el resultado es:

Al documentar las rutas y los controles, el endpoint queda completamente documentado y listo para entender como usarlo.

Conclusiones

En esta entrega se realizó la documentación de un API, en específico las rutas y controles que determinan el funcionamiento de un endpoint.

Se utilizó un plugin para JSDoc que permite extender la capacidad de documentación, al brindar propiedades adecuadas para este ejemplo, brindando así un mayor detalle sobre el mismo.

Finalmente se cambió la apariencia de la página web de la documentación al utilizar una plantilla.

Escribe un comentario

Comentario