English Version -> https://fredmaia.dev/creating-rest-apis-with-adonisjs-5/
AdonisJS é um framework Node.JS completo, altamente focado na experiência dos desenvolvedores, estabilidade e velocidade. Criado em 2015, inspirado por frameworks como Laravel e Rails. AdonisJS 5 possui recursos como:
Vamos criar agora um projeto chamado blog-api com AdonisJS 5 para entender alguns de seus conceitos.
AdonisJS 5 requer Node.JS >= 12.0.0, junto com NPM >= 6.0.0. Vamos usar o yarn como nosso gerenciador de pacotes e o Visual Studio Code como nosso editor.
Execute o comando abaixo para criar uma nova estrutura de projeto e instalar todas as dependências necessárias.
$ yarn create adonis-ts-app blog-api-adonisjs-5
Escolha API Project no terminal e confirme o nome do projeto. Também recomendo aceitar a instalação do ESLint. A imagem abaixo mostra a estrutura de um projeto com AdonisJS, seguindo Convenção sobre Configuração (Convention over Configuration), essa estrutura serve como um ótimo ponto de partida para o desenvolvimento de aplicações. Você pode ler mais sobre a estrutura do AdonisJS aqui.
Entre no diretório recém-criado e execute o servidor.
$ cd blog-api-adonisjs-5 && yarn start
(executa node ace serve —watch)
Abra seu navegador em http://localhost:3333
, e você deverá ver um JSON ‘Hello World’. Essa resposta é definida de uma forma bem simples em start/routes.ts
. Execute o seguinte comando para criar um build de produção.
$ yarn build
(executa node ace build —production)
Você pode ver todos os comandos disponíveis executando node ace --help
.
O AdonisJS segue a arquitetura MVC (Modelo-Visão-Controlador / Model-View-Controller) em que os controladores lidam com as solicitações HTTP. Os controladores ficam no diretório app/Controllers/Http
. O comando abaixo gera um novo controlador para o Post.
$ node ace make:controller Post
Abra o projeto usando o VS Code e adicione um método “index” ao PostsController retornando um array em memória com todos os posts.
// file: app/Controllers/Http/PostsController.ts
export default class PostsController {
public async index () {
return [
{ id: 1, title: 'First Post', content: 'This is my first blog post' },
{ id: 2, title: 'Second Post', content: 'This is my second blog post' },
]
}
}
Remova o código atual de routes.ts
e adicione uma nova rota para o método “index”. O código abaixo define uma rota para /posts
usando o método GET. O manipulador de rota (route handler) referencia o método “index” que acabamos de criar.
//file: start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.get('posts', 'PostsController.index')
Se você parou o servidor, inicie-o novamente executando yarn start
e acesse http://localhost:3333/posts
.
Melhor do que acessar pelo navegador, você pode consumir essa API de um cliente REST (REST Client) como o Postman. Postman é
uma ótima ferramenta e se você não conhece eu sugiro que comece a usá-la para testar e / ou documentar suas APIs REST.
https://www.postman.com/downloads/
Adicione um método store
ao PostsController para receber os dados e criar um novo post. Os valores virão do
corpo (body) da solicitação HTTP. Para obter os dados, precisamos extraí-los do objeto request
.
// file: app/Controllers/Http/PostsController.ts
...
public async store ({ request }: HttpContextContract) {
const data = request.all()
console.log(data)
}
O método store
é muito simples no momento, ele apenas imprime os dados do body
usando console.log ()
, vamos melhorá-lo mais tarde.
Adicione uma nova rota para o método store
. O código abaixo define uma rota para /posts
usando o método HTTP POST.
// file: start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.get('posts', 'PostsController.index')
Route.post('posts', 'PostsController.store') //new route
Agora podemos usar o Postman para enviar uma requisição POST para nossa API.
Antes de enviar a requisição, adicione um cabeçalho definindo o Content-Type
como application/json
.
Com isso podemos enviar uma requisição à API para criar um novo post.
Enviando esta requisição, você pode ver no console:
{ title: 'Third post', content: 'This is my third post' }
Usar request.all()
funciona, mas este método aceitará quaisquer dados que venham da requisição, por razões de segurança, devemos aceitar apenas os
campos que sabemos que compõem um objeto post, neste caso título (title) e conteúdo (content). Altere o método store
para
usar request.only()
em vez de request.all()
.
// file: app/Controllers/Http/PostsController.ts
...
public async store ({ request }: HttpContextContract) {
const data = request.only(['title', 'content'])
console.log(data)
}
Se fizermos uma solicitação GET para /posts
, não podemos ver o novo post, pois apenas o imprimimos no console. Para ficar um pouco mais interessante, vamos manter um array
de posts na memória e adicionar este novo post ao array
de posts.
// file: app/Controllers/Http/PostsController.ts
export default class PostsController {
private static posts = [
{ id: 1, title: 'First Post', content: 'This is my first blog post' },
{ id: 2, title: 'Second Post', content: 'This is my second blog post' },
]
public async index () {
return PostsController.posts
}
public async store ({ request }: HttpContextContract) {
const data = request.only(['title', 'content'])
const newId = PostsController.posts.length + 1
const post = {
id: newId,
title: data.title,
content: data.content,
}
PostsController.posts.push(post)
return post
}
}
Agora, se enviarmos uma requisição POST
adicionando o terceiro post, e fizermos uma solicitação GET
para ver todos, podemos ver o novo post criado.
Adicione um método destroy
ao PostsController
. Para deletar uma postagem, vamos enviar uma requisição DELETE
para /posts/:id
por exemplo /posts/1
passando o id do post como parâmetro. Para obter o parâmetro id
, devemos receber um objeto params
e extrair dele.
Assim que tivermos o id
como um número, filtramos o array
de posts para remover o objeto com o id recebido.
// file: app/Controllers/Http/PostsController.ts
...
public async destroy ({ params }: HttpContextContract) {
const postId = Number(params.id) //transform to number
PostsController.posts = PostsController.posts.filter(p => p.id !== postId)
}
Vamos testar? Ainda não. Precisamos registrar uma nova rota para a exclusão de posts.
// file: start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.get('posts', 'PostsController.index')
Route.post('posts', 'PostsController.store')
Route.delete('posts/:id', 'PostsController.destroy') //new route
Agora podemos enviar uma requisição DELETE
para a API remover um post.
Adicione um método show
ao PostsController
. Para buscar um post iremos enviar uma requisição GET
para /posts/:id
passando o id do post como parâmetro. Bem, já sabemos como fazer isso.
// file: app/Controllers/Http/PostsController.ts
...
public async show ({ params }: HttpContextContract) {
const postId = Number(params.id)
return PostsController.posts.find(p => p.id === postId)
}
Muito simples, certo? Vamos evoluir um pouco. O AdonisJS tem várias convenções e uma delas é relacionada às rotas.
AdonisJS fornece um atalho para definir todas as rotas RESTful usando Route resources.
Se implementarmos todas as operações de CRUD (criar, ler, atualizar, excluir), teremos um mapeamento para cada uma delas e
se usarmos o Adonis Template Engine (Edge), precisaremos de ainda mais mapeamentos para navegar entre as páginas. Para simplificar isso, podemos
usar o método Route.resource()
. Substitua o seu routes.ts
pelo código abaixo.
// file: start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.resource('posts', 'PostsController')
O código acima registra as seguintes rotas junto aos seus métodos do controller.
No entanto, ao criar uma API, não precisamos de rotas para exibir nenhuma página como create
e edit
.
Podemos removê-los usando o método apiOnly()
.
// file: start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.resource('posts', 'PostsController').apiOnly()
Nossas rotas atuais agora são as seguintes:
Ainda temos o método update
registrado para PUT
e PATCH
, mas não o implementamos (dever de casa?), você pode removê-lo
alterando o código do routes.ts
para:
// file: start/routes.ts
import Route from '@ioc:Adonis/Core/Route'
Route.resource('posts', 'PostsController')
.except(['update'])
.apiOnly()
Ótimo! Você pode estar se perguntando, que tal usar um banco de dados? Cada vez que reiniciamos o servidor os posts registrados são perdidos e voltam aos valores originais. Vamos dar esse próximo passo, continue comigo. :)
AdonisJS 5 tem suporte de primeira classe para bancos de dados SQL. A camada de banco de dados do framework (Lucid) vem com um conjunto de ferramentas versátil, permitindo-nos construir aplicativos orientados a dados de forma rápida e fácil. Lucid vem com uma implementação do padrão Active Record e suporta os principais bancos de dados relacionais.
Para instalar e inicializar o Lucid, execute os comandos abaixo:
$ yarn add @adonisjs/lucid@alpha
$ node ace invoke @adonisjs/lucid
Os comandos acima criarão o arquivo de configuração padrão e registrarão @adonisjs/lucid
ao array de provedores (providers).
Iremos usar o SQLite como nosso banco de dados, você pode instalá-lo com o comando abaixo:
$ yarn -D add sqlite3
Em config/database.ts
você pode ver as opções de configuração. Existem exemplos para diversos
bancos de dados, mas o que define a opção escolhida é seu arquivo .env
. Certifique-se de que ele tem a propriedade
DB_CONNECTION=sqlite
.
O arquivo de banco de dados fica dentro da pasta tmp
. Portanto, crie o diretório tmp
dentro da raíz do seu projeto.
$ mkdir tmp
Para ter certeza de que seus comandos estão atualizados, construa o projeto com yarn build
.
Em seguida, execute o seguinte comando para criar seu primeiro modelo de dados:
$ node ace make:model Post
Ele criará um novo modelo no diretório app/Models
com o seguinte conteúdo:
// file: app/Models/Post.ts
import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
export default class Post extends BaseModel {
@column({ isPrimary: true })
public id: number
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}
Esta é uma classe padrão, você pode remover as propriedades se precisar. No nosso caso, vamos mantê-las, e iremos apenas adicionar duas novas propriedades para o título e o conteúdo.
// file: app/Models/Post.ts
import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'
export default class Post extends BaseModel {
@column({ isPrimary: true })
public id: number
@column()
public title: string
@column()
public content: string
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}
Nós criamos o modelo de post e seguindo a convenção do AdonisJS ele o mapeia automaticamente para uma tabela de banco de dados chamada posts
.
Esta tabela não é criada automaticamente pelo Adonis, precisamos usar as migrações (migrations) para isso.
Schema migrations oferecem uma API robusta para evoluir e rastrear mudanças no banco de dados. Você pode criar / modificar o banco de dados apenas escrevendo Javascript / TypeScript.
Vamos executar o seguinte comando para criar um novo arquivo de migração:
$ node ace make:migration posts
Isso cria um arquivo em database/migrations
, no meu caso chamado 1594401640375_posts.ts
. O arquivo terá as colunas padrões que existiam no modelo de exemplo do Adonis.
// file: database/migrations/1594401640375_posts.ts
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class Posts extends BaseSchema {
protected tableName = 'posts'
public async up () {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.timestamps(true)
})
}
public async down () {
this.schema.dropTable(this.tableName)
}
}
Em seguida, adicionamos as colunas de title e content. Pelo que eu sei, o AdonisJS não sincroniza seu modelo com o arquivo de migrations (acho que Rails faz isso), precisamos adicioná-los manualmente. Então, vamos lá.
// file: database/migrations/1594401640375_posts.ts
import BaseSchema from '@ioc:Adonis/Lucid/Schema'
export default class Posts extends BaseSchema {
protected tableName = 'posts'
public async up () {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table.string('title').notNullable()
table.string('content').notNullable()
table.timestamps(true)
})
}
public async down () {
this.schema.dropTable(this.tableName)
}
}
Com o código de migrations concluído, vamos construir o aplicativo e aplicá-lo. Execute o comando abaixo:
$ yarn build && node ace migration: run
Se você executar novamente o mesmo comando, o Lucid mostrará que as migrações estão atualizadas. Tudo certo!
Agora podemos mudar nosso PostsController
para usar o modelo Post
.
O projeto está usando um array na memória para persistir os dados. Acabamos de criar nosso modelo de post usando o Lucid e configuramos o banco de dados SQLite então, vamos usá-lo!
Altere o método store
do PostsController
para o código abaixo:
// file: app/Controllers/Http/PostsController.ts
...
public async store ({ request }: HttpContextContract) {
const data = request.only(['title', 'content'])
const post = {
title: data.title,
content: data.content,
}
return await Post.create(post)
}
Removemos a linha que estávamos criando o Id e, em vez de adicionar o novo post no array, o criamos usando
o método Post.create()
. Você pode testá-lo usando as mesmas requisições que usamos antes com o Postman. Isso funcionando,
poderemos listar todas os posts.
Vamos mudar os métodos index
e show
para buscar os posts do banco de dados.
// file: app/Controllers/Http/PostsController.ts
...
public async index () {
return await Post.all()
}
public async show ({ params }: HttpContextContract) {
return await Post.find(params.id)
}
O último é o método destroy
. Precisamos buscar a postagem e, em seguida, podemos excluí-la.
// file: app/Controllers/Http/PostsController.ts
...
public async destroy ({ params }: HttpContextContract) {
const post = await Post.find(params.id)
post?.delete()
}
Isso é tudo! Você pode encontrar o projeto completo no meu Github: https://github.com/fredmaiaarantes/blog-api-adonisjs-5
Temos muito mais a explorar sobre o AdonisJS 5, como: validações (validations), relacionamento entre modelos (model relations),
construtor de buscas (query builder), inserção de dados com seeds
, testes, autenticação, e por aí vai. Me avise caso queira aprender mais.
Tem algum feedback ou sugestão? Deixe um comentário abaixo. Be kind. :)