Três formas de fazer mock da sua API com JavaScript | Entenda onde isto te ajuda

Três formas de fazer mock da sua API com JavaScript | Entenda onde isto te ajuda

Vamos criar um fluxo de trabalho para promover feedback usando frontend sem backend

🇬🇧 English version

Neste artigo, vamos ver três implementações de API mock para um frontend sem backend. O foco principal é como implementar e abordar rapidamente os motivos para fazer isto. Não importa se sua preferência é VueJS, Angular ou React, criar um “backend-less frontend” irá te ajudar a evoluir seu frontend de forma mais rápida, além de promover feedback antes da implementação real da API no backend.

Em outras palavras, não significa fazer um frontend que não precisa de um backend, ao invés disto é libertar a equipe de frontend para evoluir de forma mais rápida sem depender do backend, ou ainda, irá promover um desenvolvimento emergente, descentralizado e participativo e assim, ajudar na implementação do backend.

Graças ao Tony Lâmpada, com quem tive a sorte de trabalhar e também me influenciou positivamente neste assunto, e assim eu continuo até hoje com a prática de criar mock da API. Fica aqui um link para o post original dele.

Problemas em projetos sem API Mock

  • Principalmente quando temos as equipes de frontend e backend separadas, um exemplo seria uma equipe interna fazendo o frontend e uma empresa terceira responsável pelo backend. Neste caso, é normal haver situações onde ficamos bloqueados, ou seja, ficamos travados esperando uma alteração na API do backend para conseguir finalizar determinada tarefa no frontend.
  • No melhor cenário, a equipe do frontend pode dizer que praticamente terminou, no entanto, está esperando a API do backend ficar pronta para fazer os testes finais.
  • Com a conclusão da API do backend, vamos ter grandes chances de retrabalho na API para completar o frontend, em outras palavras, muitas vezes ao trabalhar efetivamente na tarefa do frontend, vamos nos deparar com a necessidade de um novo atributo ou alterar o formato de outro. Também é possível haver alterações nas rotas ou parâmetros da requisição de forma a atender algum requisito de UX ou performance.

Quando utilizar?

Este artigo irá apresentar formas de fazer uma API Mock, onde para cada forma temos um cenário que poderá fazer mais sentido de uso. Todos os cenários, tem como objetivo principal, minimizar os problemas mencionados acima, ou seja, permitir gerar valor e obter ciclos de feedbacks mais rápidos.

  • A primeira forma é quando temos a necessidade de uma API RESTFul para uma entidade. Por exemplo, construir rotas para cadastrar, ler, atualizar e deletar (CRUD) para uma entidade Tarefa ou Produto. Neste caso temos bibliotecas que com uma linha de comando ou poucas linhas de códigos, conseguimos ter uma API completa de forma simples.
  • Pode ser que seu caso não envolva CRUD para diversas entidades, assim entra a segunda forma, onde no mínimo precisamos de autenticação, rotas que envolvem obter, salvar dados em múltiplas entidades ou respostas com dados mistos. Desta forma, podemos construir uma estrutura em Javascript puro para retornar objetos relacionados com algumas regras de negócio.
  • E a última forma apresentada é na linha do segundo cenário, porém para o contexto de “API Design First”, ou seja, queremos gerar um artefato ou contrato que será útil para todas as equipes envolvidas no projeto, e a partir desta especificação viva (seguindo OpenAPI 3), podemos construir a API mock, bem como guiar a evolução do backend. Enfim, esta ideia de gerar um contrato por si só já é tópico para um outro artigo.

    1) O clássico json-server

Se o que você precisa é apenas imitar a API para alguns CRUDs, acredito que o json-server é uma solução incrível. Com apenas um comando para instalar e outro para servir qualquer arquivo JSON.

npm install -g json-server

json-server --watch db.json

Abaixo um exemplo de como é estruturado o arquivo db.json

# db.json
{
  "posts": [
      {
         "id": 1, 
         "title": "The json-server is awesome", 
         "author": "typicode"
      }, { 
         "id": 2, 
         "title": "Another post", 
         "author": "typicode" 
       }
  ]
}

Desta forma, temos uma API RESTFul para os “Posts”

# Obter a lista de posts
GET http://localhost:3000/posts 

# Obter detalhe do post id=1
GET http://localhost:3000/posts/1

# Enviar um {"title": "New post", "author": "test"}
# para criar um novo registro
POST http://localhost:3000/posts

Um outro ponto bem interessante, é que ganhamos uma API bem rica, ou seja, tem parâmetros para filtro, ordenação e até paginação

# Lista de posts filtrando por title com json e author=typicode
GET /posts?title=json&author=typicode

# Lista de posts, trazendo apenas a página 2
# e paginando com no máximo 200 itens
GET /posts?_page=2&_limit=200

# Lista de posts ordenado por title decrescente
GET /posts?_sort=title&_order=desc

Em resumo, com apenas um comando é possível ter uma API completa para qualquer JSON. Porém, um detalhe desta forma inicial de uso é que não é muito flexível. Geralmente vamos precisar de configurações extras, como um prefixo ou programar algumas condições com base na requisição. Assim, uma sugestão é criar uma estrutura e utilizar o json-server como módulo e não como linha de comando diretamente.

# node ApiMock/server   OR npm run mock

ApiMock
├── db.json
└── server
    └── index.js

Criar o arquivo apiMock/server/index.js que será o ponto de entrada e onde podemos adicionar algumas regras de negócio quando necessário.

# apiMock/server/index.js
const jsonServer = require('json-server')
const server = jsonServer.create()
const router = jsonServer.router('ApiMock/db.json')
const middlewares = jsonServer.defaults()
const PORT = process.env.API_MOCK_PORT || 5001

server.use(middlewares)

# Adiciona o prefixo /api para todas rotas
server.use(
  jsonServer.rewriter({
    '/api/*': '/$1',
  })
)

server.use(router)
server.listen(PORT, () => {
  console.log(`🚀 JSON Server is running on http://localhost:${PORT}`)
})

O json será o mesmo formato já apresentado, porém, deverá ficar na pasta ApiMock

# ApiMock/db.json
{
  "posts": [
      {
         "id": 1, 
         "title": "The json-server is awesome", 
         "author": "typicode"
      }, { 
         "id": 2, 
         "title": "Another post", 
         "author": "typicode" 
       }
  ]
}

Também é interessante criar uma entrada no package.json para facilitar a inicialização do servidor da ApiMock via linha de comando. Desta forma, é possível disponibilizar a api com npm run mock

# package.json
{
   ...
  "scripts": {
    "mock": "node ApiMock/server"
    ...
  },

Note que as requisições podem ser feitas agora com o prefixo /api

GET http://localhost:3000/api/posts 
GET http://localhost:3000/api/posts/1

Assim é a implementação de uma imitação de api usando json-server. Mais detalhes e formas de evoluir esta implementação pode ser encontrada diretamente no github da biblioteca .

Se quiser verificar uma implementação completa utilizando esta abordagem, verifique o repositório a seguir: github.com/confraria-devpro/hackernews-clon..

2) Usando JavaScript puro para fornecer uma API mock

Esta forma tem como base a estrutura encontrada no projeto D-Jà vue (criação do Tony) o qual tem um frontend VueJS e um backend Django, para mais detalhes eu recomendo acessar o projeto original ou um vídeo nos links ao final do artigo. A maior diferença para a forma anterior é, ao invés de ter outra aplicação rodando em outra porta simulando o backend, esta forma irá através do parâmetro API_MOCK definir se o cliente da api (usando axios) está na pasta api (real) ou apimock, no segundo caso, dentro da aplicação tem arquivos em javascript fornecendo o resultado simulando a api real.

Primeiro, precisamos criar o dicionário que é irá gerar o JSON:

# db_todos.js
export const todos = {
  todos: [
    {description: 'Do the laundry', done: true},
    {description: 'Walk the dog', done: false}
  ]
}

Depois vamos precisar de uma classe utilitária para encapsular a “Promise” que temos na API real

# mockutils.js
export function mockasync (data) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(data), 600)
  })
}

export function mockasyncerror () {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('Something is wrong')), 600)
  })
}

Também vamos precisar declarar o ponto de entrada com os métodos correspondentes da api real (cópia), porém retornando os dados dos arquivos db_algumacoisa usando a função mockasync. Também é possível retornar dados completos, parciais, simular erros ou até mesmo latência na conexão.

# index.js

import {todos} from './db_todos'
import {mockasync} from './mockutils'

const keepLoggedIn = true

export default {
  settings () {
    return mockasync({
      SENTRY_DSN_FRONT: ''
    })
  },
  list_todos () {
    return mockasync(todos)
  },
  add_todo (newtask) {
    return mockasync({description: newtask, done: false})
  },
}

Por último, vamos precisar em algum lugar nas configurações do projeto criar um apelido que saberá apontar para api real ou api mock dependendo da variável de ambiente API_MOCK.

const _apimock = process.env.API_MOCK === '1' || (process.env.API_MOCK === undefined && _isdev)
const _apijs = _apimock ? 'apimock' : 'api'

Um exemplo funcional desta forma pode ser encontrada no repositório abaixo github.com/buserbrasil/djavue-python-brasil..

3) API Design First Approach

Esta última forma pode parecer em um primeiro momento mais complicada ou talvez um excesso de componentes, mas é mais simples do que parece. Está organizada em rotas, controladores e os dados (router, controllers e data), tudo isto tendo como base o “API Design First Approach” que também está relacionado com OpenAPI 3 (OAS3), ou seja, além de promover colaboração e descentralização, conta com um contrato (YAML) que pode ser lido por humanos e máquinas. Também vou deixar alguns links sobre o assunto no final. Na prática, fazer a API MOCK no frontend envolve alterar o YAML e implementar as rotas seguindo o contrato, ou seja, se a implementação estiver incompleta ou incorreta, será possível identificar as divergências. A documentação (swagger) é gerada automaticamente a partir do YAML.

Estrutura de pastas

api
└── mock
    └── server
        ├── config.js
        ├── controllers             👈 3) Fake logic to return data
        │   └── news.js
        ├── data                    👈 4) Mock data
        │   ├── index.js
        │   └── news.json
        ├── datahelper.js
        ├── index.js                👈 2) Routers and initialization
        └── openapi.yaml            👈 1) Contract (docs and validate the mock api)

O ponto inicial é o contrato (YAML) que segue o OpenAPI 3 (OAS3) onde as rotas e todos os detalhes referente a entrada e saída são definidas

# openapi.yaml
openapi: 3.0.2

info:
  version: 0.0.1
  title: Hackernews Mock API
  description: Mock API

paths:
  /api/news:
    get:
      operationId: hackernews.api.news.list_news
      summary: Returns the latest news
      responses:
        200:
          description: The lastest news
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                      example: 42
                    title:
                      type: string
                      example: Python is a great option for backend and APIs
                    description:
                      type: string
                      nullable: true
                    created_at:
                      type: string
                      format: date-time
                      example: 2021-01-11T11:32:28Z

E o ponto de entrada para execução é o index.js, o qual usa o express como base e inicializa tudo que será necessário para gerar as rotas com validação seguindo o contrato e a documentação.

# server/index.js
const express = require('express')
const swaggerUi = require('swagger-ui-express')
const YAML = require('yamljs')
const cors = require('cors')
const logger = require('morgan')
const bodyParser = require('body-parser')
const { OpenApiValidator } = require('express-openapi-validator')

const config = require('./config')
const swaggerDocument = YAML.load(config.OPENAPI_SPEC)

const news = require('./controllers/news')     👈 Link para item 3)

const app = express()

app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.text())
app.use(bodyParser.json())

app.use(logger('dev'))
app.use(cors({ credentials: true, origin: config.ORIGIN_URL }))
app.use('/api/docs/', swaggerUi.serve, swaggerUi.setup(swaggerDocument))

new OpenApiValidator({
  apiSpec: config.OPENAPI_SPEC,
  validateRequests: true,
  validateResponses: false,
})
  .install(app)
  .then(() => {
    app.get('/api/news', news.listNews)    👈 Impl. rotas do contrato

    http: app.listen(config.PORT, () => {
      console.log(
        '🆙 JSON Server is running on port: ' + config.PORT
      )
    })
  })

# config.js (para melhor flexibilidade)
module.exports = {
  PORT: process.env.API_PORT || 5001,
  ORIGIN_URL: process.env.ORIGIN_URL || '*',
  OPENAPI_SPEC: 'api/mock/server/openapi.yaml',
}

As rotas definidas no arquivo de inicialização faz referência para um controller, por exemplo, a rota /api/news faz chamada para news.listNews. A sintaxe dos controllers são as mesmas do express com regras para retornar conjuntos específicos de dados ou algum erro conforme a requisição

# controllers/news.js
const data = require('../data')

module.exports = {
  listNews: (req, res) => {
    const result = data.news
    res.send({ result })
  },
}

Por último, vamos precisar de uma pequena classe utilitária para converter javascript em JSON e as classes com os dados

# data/index.js
const datahelper = require('../datahelper')

module.exports = {
  news: datahelper.parseJson('./data/news.json'),
}

# data/news.json
[
  {
    "id": 1,
    "title": "Confraria DevPro faz setup de projeto VueJS",
    "author": "huogerac",
    "reactions": 42,
    "comments": [],
    "date": "2021-10-06 21:42:26"
  },
  {
    "id": 2,
    "title": "Hashnode is awesome",
    "author": "vyrotek",
    "reactions": 149,
    "comments": [],
    "date": "2021-10-05 21:32:23"
  }
]

# datahelpers.js
const fs = require('fs');
const path = require('path');

module.exports = {
  parseJson: (jsonPath) => {
    return JSON.parse(
      fs.readFileSync(path.resolve(__dirname, jsonPath), 'utf8')
    );
  }
};

A api mock pode ser executada com o comando node api/mock/server o qual pode ser adicionado no package.json. A documentação da api pode ser acessada através da URL http://localhost:5001/api/docs

Um exemplo desta implementação pode ser encontrada no repositório: github.com/confraria-devpro/hackernews-clone

Eu espero que este artigo te forneça ideias para um melhor fluxo de desenvolvimento.

Comentários e adicionar uma reação ao post são o combustível para animar quem cria conteúdo 👍

Críticas e sugestões são bem vindas também.

Valeu

Links: