Criando CRUD e relações em API REST no AdonisJS

AdonisJS 3 de Jul de 2018

Esse post é a segunda parte da série de posts “Clone AirBnB com AdonisJS, React Native e ReactJS” onde iremos construir do zero uma aplicação tanto web quando mobile com dados servidos através de uma API REST feita com NodeJSutilizando o framework AdonisJS.

Até agora já temos nosso banco de dados configurado com cadastro e autenticação de novos usuários. Se você não leu o primeiro post deixei um ponto de partida do código pronto aqui: https://github.com/rocketseat/blog-adonis-reactjs-react-native-airbnb/tree/auth.

Nessa segunda etapa iremos criar os seguintes recursos:

  • Listagem de imóveis;
  • Exibição de um único imóvel com imagens;
  • Remoção de imóveis;
  • Relacionamento entre usuários e imóveis (um para muitos);
  • Relacionamento entre imóveis e imagens (um para muitas);
  • (tudo isso utilizando API REST)

Criando models e migrations

Já que estamos falando de uma nova entidade em nossa aplicação (Imóvel), precisamos criar a tabela no banco de dados que irá armazenar esses valores além de criar o seu respectivo model que será usado para realizarmos as ações do ORM como busca, cadastro, edição, remoção, filtros, etc…

Ah, vou manter o padrão de ter tudo em inglês por isso vou chamar o imóvel de “Property” dentro da nossa API.

Para iniciar vamos criar nosso model, migration e controller executando o comando:

adonis make:model Property -m -c

Você receberá um feedback como o seguinte:

✔ create  app/Models/Property.js
✔ create  database/migrations/1530569796148_property_schema.js
✔ create  app/Controllers/Http/PropertyController.js

Wowwww! Acabamos de criar todos os arquivos relacionados à manipulação de imóveis com um único comando!

Agora com a estrutura pronta, vamos adicionar os campos à tabela de imóveisalterando nossa migration (*_property_schema.js) para incluir também os campos:

  • user_id (Referência ao usuário que criou o imóvel);
  • title (Título do imóvel);
  • address (Endereço completo);
  • price (Preço da diária);
  • latitude;
  • longitude;
'use strict'

const Schema = use('Schema')

class PropertySchema extends Schema {
  up () {
    this.create('properties', table => {
      table.increments()
      table
        .integer('user_id')
        .unsigned()
        .references('id')
        .inTable('users')
        .onUpdate('CASCADE')
        .onDelete('CASCADE')
      table.string('title').notNullable()
      table.string('address').notNullable()
      table.decimal('price').notNullable()
      table.decimal('latitude', 9, 6).notNullable()
      table.decimal('longitude', 9, 6).notNullable()
      table.timestamps()
    })
  }

  down () {
    this.drop('properties')
  }
}

module.exports = PropertySchema

A maioria dos campos não tem muito segredo mas podemos prestar atenção no campo “user_id” que possui um relacionamento com a tabela de usuários. Além disso, os campos de latitude e longitude são valores decimais com 6 números após a vírgula.

Agora podemos criar nossa tabela no banco de dados com o comando: adonis migration:run.

Pronto, já podemos prosseguir para a criação da nossa segunda migration que criará a tabela no banco responsável por armazenar as imagens de cada imóvel. Como os imóveis poderão ter inúmeras imagens vamos separar isso em outra tabela com novamente um relacionamento 1-N.

Dessa vez não criaremos o controller já que as imagens serão enviadas junto com os demais dados na criação de um imóvel:

adonis make:model Image -m

Agora vamos alterar a migration para conter a referência à tabela de imóveis e também um campo para armazenar o caminho físico da imagem na nossa aplicação:

'use strict'

const Schema = use('Schema')

class ImageSchema extends Schema {
  up () {
    this.create('images', table => {
      table.increments()
      table
        .integer('property_id')
        .unsigned()
        .references('id')
        .inTable('properties')
        .onUpdate('CASCADE')
        .onDelete('CASCADE')
      table.string('path').notNullable()
      table.timestamps()
    })
  }

  down () {
    this.drop('images')
  }
}

module.exports = ImageSchema

Relacionamentos no AdonisJS

Agora que configuramos nossas tabelas no banco de dados vamos informar nos nossos models os relacionamentos existentes entre as tabelas para quando realizarmos algum cadastro de informação posteriormente o Adonis ficar sabendo dessas relações.

No arquivo app/Models/User.js vamos começar informando que um usuário pode ter muitos imóveis cadastrados, adicionando um método no fim da classe (não apague código, apenas adicione):

'use strict'

const Model = use('Model')
const Hash = use('Hash')

class User extends Model {
  // código existente

  properties () {
    return this.hasMany('App/Models/Property')
  }
}

module.exports = User

Veja que para criamos um relacionamento no model sempre criamos um novo método retornando uma relação que pode ser “1-N”, “1-1”, “N-N” ou até relacionamentos polimórficos ou mais avançados. Outro ponto é que referenciamos outro model da aplicação em formato de string mesmo.

Agora no model de imóvel vamos adicionar o relacionamento contrário informando que o imóvel sempre pertence a um usuário e aproveitando pra adicionar o relacionamento que um imóvel possui muitas imagens:

'use strict'

const Model = use('Model')

class Property extends Model {
  user () {
    return this.belongsTo('App/Models/User')
  }

  images () {
    return this.hasMany('App/Models/Image')
  }
}

module.exports = Property

Um último caso seria adicionarmos o relacionamento que a imagem pertence à um imóvel, porém nesse caso não é necessário. Isso porque jamais iremos criar a imagem antes para depois relacioná-la com um imóvel, o caminho é sempre inverso, criaremos o imóvel e junto com ele suas imagens, ou seja, não precisamos do relacionamento contrário🙂

Rotas e controllers

Agora com nossos models devidamente configurados vamos partir para adicionar a lógica do negócio. Todas nossas ações daqui pra frente vão girar em cima do controller de imóvel, por isso, abra o arquivo PropertyController.

Vamos começar removendo dois métodos desse arquivo: create e edit que são funções inúteis em um ambiente de API REST já que servem apenas para exibir os formulários de criação e edição respectivamente, os quais não existem em uma API.

Cada método que sobrou tem uma responsabilidade:

  • index: Listar todos registros;
  • show: Exibir um registro;
  • store: Criar novo registro;
  • update: Alterar um registro;
  • destroy: Remover um registro;

Antes de continuarmos vamos criar as rotas para cada um desses métodos no arquivo start/routes.js. Ao invés de criar uma rota para cada método o Adonis nos oferece um helper chamado resource que pode ser usado da seguinte forma:

Route.resource('properties', 'PropertyController')
  .apiOnly()
  .middleware('auth')

Nesse caso estamos informando para o Adonis criar todas as rotas de listagem, exibição, criação, edição e remoção de imóveis em um único comando. O método apiOnly() garante as rotas create e edit que deletamos anteriormente não tenham rota, já o middlewareauth vai garantir que usuários não autenticados não possam utilizar essas rotas.

Configurando Insomnia

Para testar a API que estamos desenvolvendo eu deixei um arquivo que você pode importar no seu Insomnia que já contém todos métodos que iremos ter no nosso controller. Basta ir na seção “Import” no Insomnia na opção de “Import from URL” e informar o seguinte endereço: https://raw.githubusercontent.com/Rocketseat/blog-adonis-reactjs-react-native-airbnb/imovel/airbnb-api.json

Pronto, você terá todas as rotas disponíveis na aplicação já com parâmetros fictícios para serem enviados em cada rota. A única coisa que você precisa fazeré se autenticar com um usuário, copiar o token JWT e colar na seção environment do Insomnia:

Ah, você pode alterar a URL também se a sua for diferente ou se estiver testando a API online 🙂

Criando CRUD

Agora vamos botar a mão na massa, se você não conhece o termo CRUD, é uma sigla para (Create, Read, Update, Detele), ou seja, operações básicas em um model. No controller de imóveis adicione a seguinte linha logo após a notação 'use strict' para termos acesso ao model de imóvel dentro do código:

const Property = use('App/Models/Property')

Vou começar com os métodos index, show e destroy que são mais simples e depois partimos para os dois restantes:

async index () {
  const properties = Property.all()

  return properties
}

Por enquanto vamos apenas retornar todos os imóveis nesse método (mais tarde iremos buscar apenas imóveis próximos baseados na localização do usuário).

Execute no Insomnia a requisição “Index” na pasta “Imóveis”, um array vaziodeverá ser retornado.

Nesse momento, seria legal você adicionar manualmente alguns registros de imóveis e imagens de imóveis no seu banco de dados.

async show ({ params }) {
  const property = await Property.findOrFail(params.id)

  await property.load('images')

  return property
}

O método show possui algumas peculiaridades comparado ao anterior. Nesse caso, podemos acessar os parâmetros enviados na URL (ex: /properties/1) através da variável params e assim buscar exatamente o registro no banco com esse ID.

Logo após, executamos o método load para fazer o carregamento de um relacionamento do model, dessa forma, nossa API retornará também as imagens do imóvel buscado.

Executando a ação “Show” na pasta “Imóvel” no Insomnia obteremos um erro HTTP já que não temos nenhum imóvel cadastrado, mas você pode experimentar cadastrar um imóvel com imagens e terá um resultado semelhante a esse:

Agora, o último método que faremos nesse post é o destroy que serve para deletar um registro:

async destroy ({ params, auth, response }) {
  const property = await Property.findOrFail(params.id)

  if (property.user_id !== auth.user.id) {
    return response.status(401).send({ error: 'Not authorized' })
  }

  await property.delete()
}

Esse método por sua vez novamente busca o imóvel através do parâmetro ID da URL e faz uma verificação se o dono do imóvel é o mesmo usuário tentando fazer a requisição, caso contrário não permite a operação retornando a mensagem:

No fim, caso validado, o imóvel é deletado.

Mais um passo

Nesse post dêmos mais um passo para a finalização da nossa aplicação completa com AdonisJS, ReactJS e React Native e agora estamos enxergando cada vez mais como será realizada a comunicação entre essas plataformas utilizando nossa API REST.

Código desse post: https://github.com/Rocketseat/blog-adonis-reactjs-react-native-airbnb/tree/imovel

Para o próximo post dessa série nós vamos mais a fundo na nossa API construindo as seguintes funcionalidades:

  • Criação/edição de imóveis com upload;
  • Busca por distância através de GPS;

Se você está curtindo essa série não esquece de deixar um comentário aí em baixo pra eu ficar sabendo e claro, todas dúvidas também podem aparecer aí nos comentários

Marcadores

Diego Fernandes

Programador full-stack, apaixonado pelas melhores tecnologias de desenvolvimento back-end, front-end e mobile, é co-fundador e CTO na Rocketseat.