12k
All articles

Como Construir uma API CRUD com AdonisJS

Crie uma API CRUD com AdonisJS v7: rotas de posts, modelos Lucid, validação VineJS e respostas JSON testadas com curl.

OpenReplay Team
OpenReplay Team
Como Construir uma API CRUD com AdonisJS

AdonisJS é um framework Node.js com TypeScript em primeiro lugar, inspirado no Laravel, que inclui roteamento, um ORM (Lucid), validação (VineJS), autenticação e uma CLI (ace) em um único pacote — o que o torna uma excelente opção para APIs CRUD onde se prefere convenções a configurações manuais. Este guia constrói um endpoint CRUD completo para posts com o AdonisJS v7, cobrindo todas as etapas, desde o scaffold até uma requisição curl testada, incluindo validação com VineJS e respostas de erro estruturadas.

O AdonisJS v7 difere do v6 em três aspectos que este tutorial reflete: configuração (Node.js 24+, npm create adonisjs@latest); o starter de API (um monorepo Turborepo com o backend em apps/backend/, com Lucid ORM e autenticação pré-configurados); e padrões de código (roteamento através de #generated/controllers, modelos Lucid estendendo classes de schema geradas a partir de apps/backend/database/schema.ts). Os exemplos abaixo seguem essa estrutura do v7 ao longo de todo o tutorial.

Principais Conclusões

  • O starter de API atual do AdonisJS v7 é gerado com npm create adonisjs@latest my-api -- --kit=api, e não com a forma antiga do v6 npm init adonisjs@latest ... -- -K=api.
  • O AdonisJS v7 requer Node.js 24+ e npm 11+, portanto, atualize seu ambiente local antes de gerar um novo projeto.
  • Os novos starter kits do v7 incluem Lucid ORM, SQLite, autenticação, VineJS e configuração de testes prontos para uso, de modo que não é necessário adicionar o Lucid manualmente em um starter de API recém-criado.
  • Os exemplos de roteamento atuais do v7 utilizam o arquivo barrel gerado para controllers, import { controllers } from '#generated/controllers', e vinculam rotas com handlers no estilo [controllers.Posts, 'index'].
  • Falhas de validação do VineJS em requisições de API retornam HTTP 422 com um array errors estruturado, contendo nomes de campos, regras que falharam e mensagens — o formato que bibliotecas de formulários frontend esperam para exibir erros por campo.
  • O método Post.findOrFail(id) do Lucid lança uma exceção de registro não encontrado que o AdonisJS renderiza automaticamente como uma resposta JSON 404, sem necessidade de verificação manual de nulo.

Pré-requisitos

O AdonisJS v7 requer Node.js 24 ou superior e npm 11 ou superior. Verifique as versões locais antes de gerar o projeto:

node --version   # deve ser >= 24.x
npm --version    # deve ser >= 11.x

Os starter kits do v7 incluem o Lucid configurado com SQLite por padrão, portanto, não é necessário começar com PostgreSQL apenas para concluir este tutorial. O SQLite grava em um arquivo local e é perfeito para um guia CRUD descartável. Caso você migre posteriormente para PostgreSQL, MySQL ou outro banco de dados SQL, o Lucid ainda suporta esses drivers; o código do controller e de validação abaixo permanece o mesmo.

Gerar o projeto com o starter kit de API

O starter de API do AdonisJS v7 é gerado com npm create adonisjs@latest <nome> -- --kit=api. Execute agora:

npm create adonisjs@latest my-api -- --kit=api

A flag --kit=api seleciona o starter kit de API. Ele fornece um projeto focado no backend com padrões amigáveis para APIs, incluindo Lucid ORM, validação VineJS, autenticação, CORS, testes e um banco de dados SQLite padrão.

Acesse o diretório do projeto e inicie o servidor de desenvolvimento:

cd my-api
npm run dev

O backend é executado em http://localhost:3333 por padrão. O starter de API também inclui endpoints de autenticação em /api/v1/auth, mas este tutorial mantém os endpoints de posts públicos para que você possa se concentrar no CRUD primeiro.

Utilizar a configuração padrão do Lucid

Lucid é o ORM SQL do AdonisJS. Em tutoriais mais antigos, você frequentemente verá uma etapa de configuração manual como esta:

node ace add @adonisjs/lucid

Não execute esse comando em um starter do v7 recém-criado. O Lucid já está instalado e configurado, e o starter utiliza SQLite por padrão. Você pode ir diretamente para a criação de uma tabela.

Gerar uma migration

Uma migration é uma alteração de schema versionada. Gere uma migration para a tabela posts:

node ace make:migration posts

O comando cria um arquivo com timestamp em database/migrations/. Abra-o e defina as colunas da tabela:

import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
  protected tableName = 'posts'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id')
      table.string('title').notNullable()
      table.text('body').notNullable()
      table.timestamp('created_at')
      table.timestamp('updated_at')
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}

O caminho de importação ainda é @adonisjs/lucid/schema. A mudança importante do v7 ocorre após a execução da migration: o Lucid gera classes de schema a partir do seu banco de dados para que o modelo da aplicação possa herdar definições de colunas tipadas.

Executar a migration e criar o model

Aplique o schema ao banco de dados:

node ace migration:run

O Lucid cria a tabela posts e gera database/schema.ts. Esse arquivo gerado contém uma classe PostsSchema com definições de colunas tipadas para a tabela. Não edite database/schema.ts diretamente; ele é regenerado após as migrations.

Agora crie o model:

node ace make:model Post

Abra app/models/post.ts e faça o model estender a classe de schema gerada, exportada de database/schema.ts:

import { PostSchema } from '#database/schema'

export default class Post extends PostSchema {}

Esse é o padrão de model Lucid do v7. Os decoradores de colunas ficam na classe de schema gerada, enquanto o model da aplicação é onde você adiciona relacionamentos, hooks, escopos de consulta e métodos personalizados posteriormente.

Gerar um controller

node ace make:controller posts --resource gera um controller com recursos para o recurso posts. Execute agora:

node ace make:controller posts --resource

A flag --resource gera os métodos REST convencionais: index, create, store, show, edit, update e destroy. Para uma API JSON, você precisa apenas de cinco deles:

MétodoFinalidade
indexListar todos os posts
storeCriar um novo post a partir do corpo da requisição
showRetornar um único post por id
updateModificar um post existente por id
destroyExcluir um post por id

Você pode excluir os métodos create e edit gerados, pois eles existem para formulários HTML renderizados no servidor.

Configurar as rotas

A documentação atual do v7 mostra controllers sendo importados através do arquivo barrel gerado, #generated/controllers. Abra start/routes.ts e adicione as rotas de posts:

import router from '@adonisjs/core/services/router'
import { controllers } from '#generated/controllers'

router
  .group(() => {
    router.get('/posts', [controllers.Posts, 'index'])
    router.post('/posts', [controllers.Posts, 'store'])
    router.get('/posts/:id', [controllers.Posts, 'show'])
    router.route('/posts/:id', ['PUT', 'PATCH'], [controllers.Posts, 'update'])
    router.delete('/posts/:id', [controllers.Posts, 'destroy'])
  })
  .prefix('/api/v1')

Isso registra os endpoints da API em /api/v1/posts, seguindo o estilo de API versionada utilizado pelo starter do v7.

MétodoCaminhoMétodo do ControllerDescrição
GET/api/v1/postsindexListar todos os posts
POST/api/v1/postsstoreCriar um post
GET/api/v1/posts/:idshowBuscar um post
PUT/api/v1/posts/:idupdateAtualizar um post
PATCH/api/v1/posts/:idupdateAtualizar parcialmente um post
DELETE/api/v1/posts/:iddestroyExcluir um post

O AdonisJS ainda documenta router.resource() para rotas RESTful, mas uma rota de recurso simples registra sete rotas, incluindo GET /posts/create e GET /posts/:id/edit. A lista explícita de rotas acima mantém a superfície da API exclusivamente JSON e evita a exposição acidental de rotas para exibição de formulários.

Verifique as rotas ativas com:

node ace list:routes

Se as rotas não aparecerem, reinicie o servidor de desenvolvimento para que o arquivo barrel de controllers gerado seja atualizado.

Adicionar validação com VineJS

VineJS é a biblioteca de validação utilizada pelo AdonisJS. Gere um arquivo de validador:

node ace make:validator post

Abra app/validators/post.ts e defina os validadores para criação e atualização de posts:

import vine from '@vinejs/vine'

export const createPostValidator = vine.create({
  title: vine.string().trim().minLength(3),
  body: vine.string().trim().minLength(1),
})

export const updatePostValidator = vine.create({
  title: vine.string().trim().minLength(3).optional(),
  body: vine.string().trim().minLength(1).optional(),
})

O guia de validação atual do AdonisJS utiliza vine.create() para definir validadores. Nos controllers, request.validateUsing() executa o validador contra o corpo da requisição e retorna o payload tipado e validado.

Quando request.validateUsing() falha em uma requisição de API, o AdonisJS retorna HTTP 422 com um array JSON errors estruturado. Cada erro inclui o nome do campo, a regra que falhou e uma mensagem. Esse é o formato que o código frontend pode mapear diretamente para o estado dos campos do formulário.

Replays de sessão de frontends que consomem APIs CRUD frequentemente revelam um modo de falha específico: um formulário é enviado, o servidor retorna 422, mas nenhuma mensagem de erro por campo aparece na interface. A causa raiz geralmente é que o servidor retornou uma string simples em vez de erros estruturados por campo. O VineJS fornece o formato estruturado por padrão, portanto, preserve-o. Não capture a exceção de validação e relance uma string achatada, a menos que você também preserve o contrato por campo.

Implementar os métodos do controller

Cada ação do controller é um handler conciso. Os dois caminhos de falha relevantes — um registro ausente e entrada inválida — são tratados pelo Lucid e pelo VineJS sem ramificações manuais.

Abra app/controllers/posts_controller.ts e substitua seu conteúdo:

import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
import { createPostValidator, updatePostValidator } from '#validators/post'

export default class PostsController {
  async index({ response }: HttpContext) {
    const posts = await Post.all()
    return response.json(posts)
  }

  async store({ request, response }: HttpContext) {
    const payload = await request.validateUsing(createPostValidator)
    const post = await Post.create(payload)
    return response.created(post)
  }

  async show({ params, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    return response.json(post)
  }

  async update({ params, request, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    const payload = await request.validateUsing(updatePostValidator)

    await post.merge(payload).save()

    return response.json(post)
  }

  async destroy({ params, response }: HttpContext) {
    const post = await Post.findOrFail(params.id)
    await post.delete()

    return response.noContent()
  }
}

Alguns detalhes são importantes:

  • store retorna response.created(post), que define HTTP 201.
  • destroy retorna response.noContent(), que define HTTP 204.
  • await post.delete() é aguardado, de modo que a resposta não retorna antes que a exclusão seja concluída.
  • Post.findOrFail(params.id) lança uma exceção quando nenhuma linha corresponde. O AdonisJS converte essa exceção automaticamente em uma resposta 404.

Testar com curl

Uma API CRUD do AdonisJS v7 pode ser exercitada de ponta a ponta com curl — cada ação de recurso (index, store, show, update, destroy) corresponde a uma única requisição HTTP em /api/v1/posts. Com npm run dev em execução, crie um post:

curl -X POST http://localhost:3333/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "First post", "body": "Hello from Adonis"}'

Uma resposta bem-sucedida de store retorna HTTP 201 com o registro criado:

{
  "title": "First post",
  "body": "Hello from Adonis",
  "createdAt": "2026-06-01T12:00:00.000+00:00",
  "updatedAt": "2026-06-01T12:00:00.000+00:00",
  "id": 1
}

Agora acione erros de validação enviando um título muito curto e um corpo vazio:

curl -X POST http://localhost:3333/api/v1/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "ok", "body": ""}'

A resposta é HTTP 422 com um array de erros VineJS estruturado:

{
  "errors": [
    {
      "field": "title",
      "rule": "minLength",
      "message": "The title field must have at least 3 characters"
    },
    {
      "field": "body",
      "rule": "minLength",
      "message": "The body field must have at least 1 characters"
    }
  ]
}

Esse é o contrato que um frontend mapeia para o estado do formulário: itere sobre errors, indexe por field e renderize message ao lado do campo correspondente.

Busque um registro inexistente para ver o caminho do 404:

curl -i http://localhost:3333/api/v1/posts/9999

findOrFail produz uma resposta 404 em vez de retornar null com um 200 enganoso.

As demais requisições CRUD seguem o mesmo padrão:

curl http://localhost:3333/api/v1/posts                    # index
curl http://localhost:3333/api/v1/posts/1                  # show
curl -X PUT http://localhost:3333/api/v1/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated title"}'                         # update
curl -X PATCH http://localhost:3333/api/v1/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"body": "Updated body"}'                            # atualização parcial
curl -X DELETE http://localhost:3333/api/v1/posts/1         # destroy → 204

Próximos passos

Uma API CRUD do AdonisJS v7 precisa de três complementos antes de estar pronta para produção: autenticação, paginação e testes.

Para autenticação, o starter de API já inclui rotas de autenticação e uma configuração orientada a tokens de acesso. Proteja as rotas de posts com o middleware de autenticação quando elas devem estar disponíveis apenas para usuários autenticados. Se você estiver adicionando autenticação a um projeto existente que não usa o starter, o comando documentado é:

node ace add @adonisjs/auth --guard=access_tokens

Para coleções grandes, substitua Post.all() no index pela paginação do Lucid, de modo que o endpoint retorne uma página por vez em vez da tabela inteira.

Para testes, utilize o Japa, o executor de testes configurado pelo starter. O starter de API do v7 utiliza:

npm run test

Comece testando os três contratos mais importantes para o frontend: uma criação bem-sucedida retorna 201, falhas de validação retornam 422 com erros por campo, e registros ausentes retornam 404.

Perguntas Frequentes

Por que este tutorial do v7 usa rotas explícitas em vez de router.resource().apiOnly()?

A documentação atual de roteamento do AdonisJS v7 documenta prominentemente router.resource(), mas uma rota de recurso simples registra sete rotas, incluindo GET /posts/create e GET /posts/:id/edit para formulários HTML. Para uma API exclusivamente JSON, rotas explícitas GET, POST, PUT, PATCH e DELETE tornam a superfície pública da API evidente e evitam o registro acidental de rotas para exibição de formulários.

Por que node ace make:controller --resource gera sete métodos quando esta API usa apenas cinco?

A flag --resource gera o conjunto completo de controllers RESTful: index, create, store, show, edit, update e destroy. Os métodos create e edit existem para renderizar formulários HTML em aplicações com renderização no servidor. Uma API JSON precisa apenas de index, store, show, update e destroy, portanto, os métodos create e edit não utilizados podem ser excluídos com segurança.

Quais métodos HTTP o endpoint de atualização deve suportar?

Este tutorial mapeia tanto PUT quanto PATCH em /api/v1/posts/:id para o mesmo método do controller update. PUT normalmente indica uma substituição completa do recurso e PATCH uma atualização parcial, mas muitas APIs CRUD tratam ambos através da mesma lógica de validação e merge. Execute node ace list:routes para confirmar o mapeamento ativo na sua aplicação.

Um tutorial do AdonisJS v6 funcionará no v7?

Não de forma confiável. Grande parte da lógica do controller ainda parece familiar, mas o v7 altera detalhes importantes: o requisito de runtime é Node.js 24+, o scaffold usa npm create adonisjs@latest, os starter kits incluem Lucid e autenticação por padrão, os exemplos de roteamento atuais usam #generated/controllers, e os models Lucid estendem classes de schema geradas de #database/schema. Trate os tutoriais do v6 apenas como referências conceituais.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.