ActiveResource.js: Construindo um SDK JavaScript poderoso para sua API JSON, rápido
Publicados: 2022-03-11Sua empresa acabou de lançar sua API e agora quer construir uma comunidade de usuários em torno dela. Você sabe que a maioria de seus clientes estará trabalhando em JavaScript, porque os serviços que sua API fornece tornam mais fácil para os clientes criar aplicativos da web em vez de escrever tudo sozinho – Twilio é um bom exemplo disso.
Você também sabe que, por mais simples que sua API RESTful possa ser, os usuários vão querer colocar um pacote JavaScript que fará todo o trabalho pesado para eles. Eles não vão querer aprender sua API e criar cada solicitação de que precisam.
Então você está construindo uma biblioteca em torno de sua API. Ou talvez você esteja apenas escrevendo um sistema de gerenciamento de estado para um aplicativo da Web que interage com sua própria API interna.
De qualquer forma, você não quer se repetir cada vez que fizer CRUD em um de seus recursos de API, ou pior, CRUD em um recurso relacionado a esses recursos. Isso não é bom para gerenciar um SDK em crescimento a longo prazo, nem é um bom uso do seu tempo.
Em vez disso, você pode usar ActiveResource.js, um sistema JavaScript ORM para interagir com APIs. Eu o criei para suprir uma necessidade que tínhamos em um projeto: criar um SDK JavaScript no menor número de linhas possível. Isso permitiu a máxima eficiência para nós e para nossa comunidade de desenvolvedores.
Ele é baseado nos princípios por trás do ORM ActiveRecord simples do Ruby on Rails.
Princípios do SDK do JavaScript
Existem duas ideias do Ruby on Rails que guiaram o design do ActiveResource.js:
- “Convenção sobre configuração:” Faça algumas suposições sobre a natureza dos terminais da API. Por exemplo, se você tiver um recurso
Product
, ele corresponderá ao ponto de extremidade/products
. Dessa forma, o tempo não é gasto configurando repetidamente cada uma das solicitações de seu SDK para sua API. Os desenvolvedores podem adicionar novos recursos de API com consultas CRUD complicadas ao seu SDK em crescimento em minutos, não em horas. - “Exalte um código bonito:” o criador do Rails, DHH, disse isso melhor – há algo ótimo sobre um código bonito por si só. O ActiveResource.js envolve solicitações às vezes feias em um belo exterior. Você não precisa mais escrever código personalizado para adicionar filtros e paginação e incluir relacionamentos aninhados em relacionamentos para solicitações GET. Também não é necessário construir solicitações POST e PATCH que façam alterações nas propriedades de um objeto e as enviem ao servidor para atualização. Em vez disso, basta chamar um método em um ActiveResource: Chega de brincar com JSON para obter a solicitação desejada, apenas para ter que fazê-lo novamente para a próxima.
Antes de começarmos
É importante observar que, no momento da redação deste artigo, ActiveResource.js funciona apenas com APIs escritas de acordo com o padrão JSON:API.
Se você não estiver familiarizado com JSON:API e quiser acompanhar, existem muitas bibliotecas boas para criar um servidor JSON:API.
Dito isso, ActiveResource.js é mais um DSL do que um wrapper para um padrão de API específico. A interface que ele usa para interagir com sua API pode ser estendida, para que artigos futuros possam abordar como usar ActiveResource.js com sua API personalizada.
Configurando as coisas
Para começar, instale active-resource
em seu projeto:
yarn add active-resource
A primeira etapa é criar uma ResourceLibrary
para sua API. Vou colocar todos os meus ActiveResource
na pasta src/resources
:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary('http://example.com/api/v1'); export default library;
O único parâmetro obrigatório para createResourceLibrary
é a URL raiz de sua API.
O que vamos criar
Vamos criar uma biblioteca JavaScript SDK para uma API de sistema de gerenciamento de conteúdo. Isso significa que haverá usuários, postagens, comentários e notificações.
Os usuários poderão ler, criar e editar postagens; leia, adicione e exclua comentários (em postagens ou em outros comentários) e receba notificações de novas postagens e comentários.
Não vou usar nenhuma biblioteca específica para gerenciar a view (React, Angular, etc.) ou o estado (Redux, etc.), ao invés disso abstrair o tutorial para interagir apenas com sua API através de ActiveResource
s.
O primeiro recurso: usuários
Vamos começar criando um recurso de User
para gerenciar os usuários do CMS.
Primeiro, criamos uma classe de recurso User
com alguns attributes
:
// /src/resources/User.js import library from './library'; class User extends library.Base { static define() { this.attributes('email', 'userName', 'admin'); } } export default library.createResource(User);
Vamos supor, por enquanto, que você tenha um endpoint de autenticação que, assim que um usuário enviar seu e-mail e senha, retorne um token de acesso e o ID do usuário. Este endpoint é gerenciado por alguma função requestToken
. Depois de obter o ID do usuário autenticado, você deseja carregar todos os dados do usuário:
import library from '/src/resources/library'; import User from '/src/resources/User'; async function authenticate(email, password) { let [accessToken, userId] = requestToken(email, password); library.headers = { Authorization: 'Bearer ' + accessToken }; return await User.find(userId); }
Eu configurei library.headers
para ter um cabeçalho Authorization
com accessToken
para que todas as solicitações futuras da minha ResourceLibrary
sejam autorizadas.
Uma seção posterior abordará como autenticar um usuário e definir o token de acesso usando apenas a classe de recurso User
.
A última etapa da authenticate
é uma solicitação para User.find(id)
. Isso fará uma solicitação para /api/v1/users/:id
e a resposta pode ser algo como:
{ "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false } } }
A resposta de authenticate
será uma instância da classe User
. A partir daqui, você pode acessar os vários atributos do usuário autenticado, caso queira exibi-los em algum lugar do aplicativo.
let user = authenticate(email, password); console.log(user.id) // '1' console.log(user.userName) // user1 console.log(user.email) // [email protected] console.log(user.attributes()) /* { email: '[email protected]', userName: 'user1', admin: false } */
Cada um dos nomes de atributo se tornará camelCased, para se adequar aos padrões típicos de JavaScript. Você pode obter cada um deles diretamente como propriedades do objeto de user
ou obter todos os atributos chamando user.attributes()
.
Adicionando um Índice de Recurso
Antes de adicionarmos mais recursos relacionados à classe User
, como notificações, devemos adicionar um arquivo, src/resources/index.js
, que indexará todos os nossos recursos. Isso tem dois benefícios:
- Ele limpará nossas importações, permitindo-nos desestruturar
src/resources
para vários recursos em uma instrução de importação em vez de usar várias instruções de importação. - Ele inicializará todos os recursos na
ResourceLibrary
que criaremos chamandolibrary.createResource
em cada um, o que é necessário para que ActiveResource.js construa relacionamentos.
// /src/resources/index.js import User from './User'; export { User };
Adicionando um recurso relacionado
Agora vamos criar um recurso relacionado para o User
, um Notification
. Primeiro crie uma classe Notification
que belongsTo
à classe User
:
// /src/resources/Notification.js import library from './library'; class Notification extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Notification);
Em seguida, adicionamos ao índice de recursos:
// /src/resources/index.js import Notification from './Notification'; import User from './User'; export { Notification, User };
Em seguida, relacione as notificações com a classe User
:
// /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); } }
Agora, assim que recuperarmos o usuário de authenticate
, podemos carregar e exibir todas as suas notificações:
let notifications = await user.notifications().load(); console.log(notifications.map(notification => notification.message));
Também podemos incluir notificações em nossa solicitação original para o usuário autenticado:
async function authenticate(email, password) { /* ... */ return await User.includes('notifications').find(userId); }
Esta é uma das muitas opções disponíveis no DSL.
Revisando o DSL
Vamos cobrir o que já é possível solicitar apenas do código que escrevemos até agora.
Você pode consultar uma coleção de usuários ou um único usuário.
let users = await User.all(); let user = await User.first(); user = await User.last(); user = await User.find('1'); user = await User.findBy({ userName: 'user1' });
Você pode modificar a consulta usando métodos relacionais encadeados:
// Query and iterate over all users User.each((user) => console.log(user)); // Include related resources let users = await User.includes('notifications').all(); // Only respond with user emails as the attributes users = await User.select('email').all(); // Order users by attribute users = await User.order({ email: 'desc' }).all(); // Paginate users let usersPage = await User.page(2).perPage(5).all(); // Filter users by attribute users = await User.where({ admin: true }).all(); users = await User .includes('notifications') .select('email', { notifications: ['message', 'createdAt'] }) .order({ email: 'desc' }) .where({ admin: false }) .perPage(10) .page(3) .all(); let user = await User .includes('notification') .select('email') .first();
Observe que você pode compor a consulta usando qualquer quantidade de modificadores encadeados e que pode terminar a consulta com .all() , .all()
( .first()
, .last()
ou .each()
.
Você pode criar um usuário localmente ou criar um no servidor:
let user = User.build(attributes); user = await User.create(attributes);
Depois de ter um usuário persistente, você pode enviar as alterações para que sejam salvas no servidor:
user.email = '[email protected]'; await user.save(); /* or */ await user.update({ email: '[email protected]' });
Você também pode excluí-lo do servidor:
await user.destroy();
Essa DSL básica também se estende a recursos relacionados, como demonstrarei no restante do tutorial. Agora podemos aplicar rapidamente o ActiveResource.js para criar o restante do CMS: postagens e comentários.
Criando postagens
Crie uma classe de recurso para Post
e associe-a à classe User
:
// /src/resources/Post.js import library from './library'; class Post extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Post);
// /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); this.hasMany('posts'); } }
Adicione Post
ao índice de recursos também:
// /src/resources/index.js import Notification from './Notification'; import Post from './Post'; import User from './User'; export { Notification, Post, User };
Em seguida, vincule o recurso Post
em um formulário para que os usuários criem e editem postagens. Quando o usuário visita o formulário pela primeira vez para criar um novo post, um recurso Post
será construído e cada vez que o formulário for alterado, aplicamos a alteração ao Post
:

import Post from '/src/resources/Post'; let post = Post.build({ user: authenticatedUser }); onChange = (event) => { post.content = event.target.value; };
Em seguida, adicione um retorno de chamada onSubmit
ao formulário para salvar a postagem no servidor e trate os erros se a tentativa de salvar falhar:
onSubmit = async () => { try { await post.save(); /* successful, redirect to edit post form */ } catch { post.errors().each((field, error) => { console.log(field, error.message) }); } }
Editando postagens
Depois que a postagem for salva, ela será vinculada à sua API como um recurso em seu servidor. Você pode saber se um recurso é persistido no servidor chamando persisted
:
if (post.persisted()) { /* post is on server */ }
Para recursos persistentes, ActiveResource.js oferece suporte a atributos sujos, pois você pode verificar se algum atributo de um recurso foi alterado de seu valor no servidor.
Se você chamar save()
em um recurso persistente, ele fará uma solicitação PATCH
contendo apenas as alterações feitas no recurso, em vez de enviar todo o conjunto de atributos e relacionamentos do recurso para o servidor desnecessariamente.
Você pode adicionar atributos rastreados a um recurso usando a declaração de attributes
. Vamos acompanhar as alterações em post.content
:
// /src/resources/Post.js class Post extends library.Base { static define() { this.attributes('content'); /* ... */ } }
Agora, com uma postagem persistente no servidor, podemos editar a postagem e, quando o botão enviar for clicado, salvar as alterações no servidor. Também podemos desabilitar o botão enviar se nenhuma alteração tiver sido feita ainda:
onEdit = (event) => { post.content = event.target.value; } onSubmit = async () => { try { await post.save(); } catch { /* display edit errors */ } } disableSubmitButton = () => { return !post.changed(); }
Existem métodos para gerenciar um relacionamento singular como post.user()
, se quisermos alterar o usuário associado a um post:
await post.updateUser(user);
Isso é equivalente a:
await post.update({ user });
O recurso de comentários
Agora crie uma classe de recurso Comment
e relacione-a a Post
. Lembre-se de nosso requisito de que os comentários possam ser em resposta a uma postagem ou a outro comentário, portanto, o recurso relevante para um comentário é polimórfico:
// /src/resources/Comment.js import library from './library'; class Comment extends library.Base { static define() { this.attributes('content'); this.belongsTo('resource', { polymorphic: true, inverseOf: 'replies' }); this.belongsTo('user'); this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } } export default library.createResource(Comment);
Certifique-se de adicionar Comment
a /src/resources/index.js
também.
Também precisaremos adicionar uma linha à classe Post
:
// /src/resources/Post.js class Post extends library.Base { static define() { /* ... */ this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } }
A opção inverseOf
passada para a definição hasMany
para replies
indica que esse relacionamento é o inverso da definição polimórfica de belongsTo
para resource
. A propriedade inverseOf
de relacionamentos é frequentemente usada ao realizar operações entre relacionamentos. Normalmente, essa propriedade será determinada automaticamente por meio do nome da classe, mas como os relacionamentos polimórficos podem ser uma de várias classes, você mesmo deve definir a opção inverseOf
para que os relacionamentos polimórficos tenham a mesma funcionalidade dos normais.
Gerenciando comentários em postagens
A mesma DSL que se aplica aos recursos também se aplica ao gerenciamento de recursos relacionados. Agora que configuramos os relacionamentos entre postagens e comentários, há várias maneiras de gerenciar esse relacionamento.
Você pode adicionar um novo comentário a uma postagem:
onSubmitComment = async (event) => { let comment = await post.replies().create({ content: event.target.value, user: user }); }
Você pode adicionar uma resposta a um comentário:
onSubmitReply = async (event) => { let reply = await comment.replies().create({ content: event.target.value, user: user }); }
Você pode editar um comentário:
onEditComment = async (event) => { await comment.update({ content: event.target.value }); }
Você pode remover um comentário de uma postagem:
onDeleteComment = async (comment) => { await post.replies().delete(comment); }
Exibindo postagens e comentários
O SDK pode ser usado para exibir uma lista paginada de postagens e, quando uma postagem é clicada, a postagem é carregada em uma nova página com todos os seus comentários:
import { Post } from '/src/resources'; let postsPage = await Post .order({ createdAt: 'desc' }) .select('content') .perPage(10) .all();
A consulta acima irá recuperar os 10 posts mais recentes, e para otimizar, o único atributo que é carregado é o seu content
.
Se um usuário clicar em um botão para ir para a próxima página de postagens, um manipulador de alterações recuperará a próxima página. Aqui também desativamos o botão se não houver próximas páginas.
onClickNextPage = async () => { postsPage = await postsPage.nextPage(); if (!postsPage.hasNextPage()) { /* disable next page button */ } };
Quando um link para uma postagem é clicado, abrimos uma nova página carregando e exibindo a postagem com todos os seus dados, incluindo seus comentários - conhecidos como respostas - bem como as respostas a essas respostas:
import { Post } from '/src/resources'; onClick = async (postId) => { let post = await Post.includes({ replies: 'replies' }).find(postId); console.log(post.content, post.createdAt); post.replies().target().each(comment => { console.log( comment.content, comment.replies.target().map(reply => reply.content).toArray() ); }); }
Chamar .target()
em um relacionamento hasMany
como post.replies()
retornará um ActiveResource.Collection
de comentários que foram carregados e armazenados localmente.
Essa distinção é importante, porque post.replies().target().first()
retornará o primeiro comentário carregado. Em contraste, post.replies().first()
retornará uma promessa para um comentário solicitado de GET /api/v1/posts/:id/replies
.
Você também pode solicitar as respostas de uma postagem separadamente da solicitação da postagem em si, o que permite modificar sua consulta. Você pode encadear modificadores como order
, select
, includes
, where
, perPage
, page
ao consultar hasMany
relacionamentos assim como você pode ao consultar os próprios recursos.
import { Post } from '/src/resources'; onClick = async (postId) => { let post = await Post.find(postId); let userComments = await post.replies().where({ user: user }).perPage(3).all(); console.log('Your comments:', userComments.map(comment => comment.content).toArray()); }
Modificando recursos após serem solicitados
Às vezes você quer pegar os dados do servidor e modificá-los antes de usá-los. Por exemplo, você pode envolver post.createdAt
em um objeto moment()
para que você possa exibir uma data e hora amigável para o usuário sobre quando a postagem foi criada:
// /src/resources/Post.js import moment from 'moment'; class Post extends library.Base { static define() { /* ... */ this.afterRequest(function() { this.createdAt = moment(this.createdAt); }); } }
Imutabilidade
Se você trabalha com um sistema de gerenciamento de estado que favorece objetos imutáveis, todo o comportamento em ActiveResource.js pode se tornar imutável configurando a biblioteca de recursos:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary( 'http://example.com/api/v1', { immutable: true } ); export default library;
Voltando: Vinculando o Sistema de Autenticação
Para encerrar, mostrarei como integrar seu sistema de autenticação de usuário em seu User
ActiveResource
.
Mova seu sistema de autenticação de token para o endpoint da API /api/v1/tokens
. Quando o email e a senha de um usuário são enviados para este endpoint, os dados do usuário autenticado mais o token de autorização serão enviados em resposta.
Crie uma classe de recurso Token
que pertença a User
:
// /src/resources/Token.js import library from './library'; class Token extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Token);
Adicione Token
a /src/resources/index.js
.
Em seguida, adicione um método estático de authenticate
à sua classe de recurso User
e relacione User
to Token
:
// /src/resources/User.js import library from './library'; import Token from './Token'; class User { static define() { /* ... */ this.hasOne('token'); } static async authenticate(email, password) { let user = this.includes('token').build({ email, password }); let authUser = await this.interface().post(Token.links().related, user); let token = authUser.token(); library.headers = { Authorization: 'Bearer ' + token.id }; return authUser; } }
Esse método usa resourceLibrary.interface()
, que nesse caso é a interface JSON:API, para enviar um usuário para /api/v1/tokens
. Isso é válido: um ponto de extremidade em JSON:API não exige que os únicos tipos postados nele sejam aqueles que receberam o nome. Assim, o pedido será:
{ "data": { "type": "users", "attributes": { "email": "[email protected]", "password": "password" } } }
A resposta será o usuário autenticado com o token de autenticação incluído:
{ "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false }, "relationships": { "token": { "data": { "type": "tokens", "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX", } } } }, "included": [{ "type": "tokens", "id": "Qcg6yI1a5qCxXgKWtSAbZ2MIHFChHAq0Vc1Lo4TX", "attributes": { "expires_in": 3600 } }] }
Em seguida, usamos o token.id
para definir o cabeçalho Authorization
da nossa biblioteca e retornar o usuário, que é o mesmo que solicitar o usuário via User.find()
como fizemos antes.
Agora, se você chamar User.authenticate(email, password)
, receberá um usuário autenticado em resposta e todas as solicitações futuras serão autorizadas com um token de acesso.
ActiveResource.js permite o desenvolvimento rápido do SDK do JavaScript
Neste tutorial, exploramos as maneiras pelas quais o ActiveResource.js pode ajudá-lo a criar rapidamente um SDK JavaScript para gerenciar seus recursos de API e seus vários recursos relacionados, às vezes complicados. Você pode ver todos esses recursos e mais documentados no README para ActiveResource.js.
Espero que você tenha gostado da facilidade com que essas operações podem ser feitas e que você use (e talvez até contribua) minha biblioteca para seus projetos futuros, se ela atender às suas necessidades. No espírito do código aberto, os PRs são sempre bem-vindos!