ActiveResource.js: creación rápida de un potente SDK de JavaScript para su API JSON
Publicado: 2022-03-11Su empresa acaba de lanzar su API y ahora quiere crear una comunidad de usuarios a su alrededor. Sabe que la mayoría de sus clientes trabajarán en JavaScript, porque los servicios que brinda su API facilitan a los clientes la creación de aplicaciones web en lugar de escribir todo ellos mismos; Twilio es un buen ejemplo de esto.
También sabe que, por más simple que sea su API RESTful, los usuarios querrán incluir un paquete de JavaScript que hará todo el trabajo pesado por ellos. No querrán aprender su API y crear cada solicitud que necesiten ellos mismos.
Así que está construyendo una biblioteca alrededor de su API. O tal vez solo esté escribiendo un sistema de administración de estado para una aplicación web que interactúa con su propia API interna.
De cualquier manera, no desea repetirse una y otra vez cada vez que CRUD uno de sus recursos API, o peor aún, CRUD un recurso relacionado con esos recursos. Esto no es bueno para administrar un SDK en crecimiento a largo plazo, ni es un buen uso de su tiempo.
En su lugar, puede usar ActiveResource.js, un sistema ORM de JavaScript para interactuar con las API. Lo creé para satisfacer una necesidad que teníamos en un proyecto: crear un SDK de JavaScript en la menor cantidad de líneas posible. Esto permitió la máxima eficiencia para nosotros y para nuestra comunidad de desarrolladores.
Se basa en los principios detrás de ActiveRecord ORM simple de Ruby on Rails.
Principios del SDK de JavaScript
Hay dos ideas de Ruby on Rails que guiaron el diseño de ActiveResource.js:
- “Convención sobre configuración”: haga algunas suposiciones sobre la naturaleza de los puntos finales de la API. Por ejemplo, si tiene un recurso
Product
, corresponde al punto final/products
. De esa manera, no se pierde tiempo configurando repetidamente cada una de las solicitudes de su SDK a su API. Los desarrolladores pueden agregar nuevos recursos de API con consultas CRUD complicadas a su SDK en crecimiento en minutos, no horas. - “Exalte el código bello”: el creador de Rails, DHH, lo dijo mejor: hay algo grandioso en el código bello por sí mismo. ActiveResource.js envuelve solicitudes a veces feas en un hermoso exterior. Ya no tiene que escribir código personalizado para agregar filtros y paginación e incluir relaciones anidadas sobre relaciones para solicitudes GET. Tampoco tiene que construir solicitudes POST y PATCH que tomen cambios en las propiedades de un objeto y los envíen al servidor para su actualización. En su lugar, simplemente llame a un método en un ActiveResource: no más jugar con JSON para obtener la solicitud que desea, solo para tener que hacerlo nuevamente para la siguiente.
Antes que empecemos
Es importante tener en cuenta que, en el momento de escribir este artículo, ActiveResource.js solo funciona con API escritas de acuerdo con el estándar JSON:API.
Si no está familiarizado con JSON:API y desea seguirlo, hay muchas bibliotecas buenas para crear un servidor JSON:API.
Dicho esto, ActiveResource.js es más un DSL que un contenedor para un estándar de API en particular. La interfaz que usa para interactuar con su API se puede ampliar, por lo que los artículos futuros podrían cubrir cómo usar ActiveResource.js con su API personalizada.
Preparando las cosas
Para comenzar, instale active-resource
en su proyecto:
yarn add active-resource
El primer paso es crear una ResourceLibrary
para su API. Voy a poner todos mis ActiveResource
s en la carpeta src/resources
:
// /src/resources/library.js import { createResourceLibrary } from 'active-resource'; const library = createResourceLibrary('http://example.com/api/v1'); export default library;
El único parámetro requerido para createResourceLibrary
es la URL raíz de su API.
Lo que crearemos
Vamos a crear una biblioteca SDK de JavaScript para una API de sistema de administración de contenido. Eso significa que habrá usuarios, publicaciones, comentarios y notificaciones.
Los usuarios podrán leer, crear y editar publicaciones; lea, agregue y elimine comentarios (a publicaciones u otros comentarios) y reciba notificaciones de nuevas publicaciones y comentarios.
No voy a usar ninguna biblioteca específica para administrar la vista (React, Angular, etc.) o el estado (Redux, etc.), sino que abstraeré el tutorial para interactuar solo con su API a través de ActiveResource
s.
El primer recurso: usuarios
Vamos a comenzar creando un recurso de User
para administrar los usuarios del CMS.
Primero, creamos una clase de recursos de User
con algunos 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);
Supongamos por ahora que tiene un punto final de autenticación que, una vez que un usuario envía su correo electrónico y contraseña, devuelve un token de acceso y la identificación del usuario. Este punto final es administrado por alguna función requestToken
. Una vez que obtenga la ID de usuario autenticado, desea cargar todos los datos del usuario:
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); }
Configuré library.headers
para que tengan un encabezado de Authorization
con accessToken
para que todas las solicitudes futuras de mi ResourceLibrary
estén autorizadas.
Una sección posterior cubrirá cómo autenticar a un usuario y configurar el token de acceso usando solo la clase de recurso User
.
El último paso de authenticate
es una solicitud a User.find(id)
. Esto hará una solicitud a /api/v1/users/:id
, y la respuesta podría parecerse a:
{ "data": { "type": "users", "id": "1", "attributes": { "email": "[email protected]", "user_name": "user1", "admin": false } } }
La respuesta de authenticate
será una instancia de la clase User
. Desde aquí, puede acceder a los diversos atributos del usuario autenticado, si desea mostrarlos en algún lugar de la aplicación.
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 uno de los nombres de los atributos se convertirá en camelCased, para ajustarse a los estándares típicos de JavaScript. Puede obtener cada uno de ellos directamente como propiedades del objeto de user
u obtener todos los atributos llamando a user.attributes()
.
Agregar un índice de recursos
Antes de agregar más recursos relacionados con la clase User
, como notificaciones, debemos agregar un archivo, src/resources/index.js
, que indexará todos nuestros recursos. Esto tiene dos beneficios:
- Limpiará nuestras importaciones permitiéndonos desestructurar
src/resources
para múltiples recursos en una declaración de importación en lugar de usar múltiples declaraciones de importación. - Inicializará todos los recursos en
ResourceLibrary
que crearemos llamando alibrary.createResource
en cada uno, lo cual es necesario para que ActiveResource.js construya relaciones.
// /src/resources/index.js import User from './User'; export { User };
Agregar un recurso relacionado
Ahora vamos a crear un recurso relacionado para el User
, una Notification
. Primero cree una clase de Notification
que belongsTo
a la clase User
:
// /src/resources/Notification.js import library from './library'; class Notification extends library.Base { static define() { this.belongsTo('user'); } } export default library.createResource(Notification);
Luego lo agregamos al índice de recursos:
// /src/resources/index.js import Notification from './Notification'; import User from './User'; export { Notification, User };
Luego, relacione las notificaciones con la clase User
:
// /src/resources/User.js class User extends library.Base { static define() { /* ... */ this.hasMany('notifications'); } }
Ahora, una vez que recuperamos al usuario de la authenticate
, podemos cargar y mostrar todas sus notificaciones:
let notifications = await user.notifications().load(); console.log(notifications.map(notification => notification.message));
También podemos incluir notificaciones en nuestra solicitud original para el usuario autenticado:
async function authenticate(email, password) { /* ... */ return await User.includes('notifications').find(userId); }
Esta es una de las muchas opciones disponibles en el DSL.
Revisando el DSL
Veamos lo que ya es posible solicitar solo desde el código que hemos escrito hasta ahora.
Puede consultar una colección de usuarios o un solo usuario.
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' });
Puede modificar la consulta utilizando métodos relacionales encadenables:
// 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();
Tenga en cuenta que puede redactar la consulta utilizando cualquier cantidad de modificadores encadenados y que puede finalizar la consulta con .all()
() , .first( .first()
, .last()
o .each()
.
Puede crear un usuario localmente o crear uno en el servidor:
let user = User.build(attributes); user = await User.create(attributes);
Una vez que tenga un usuario persistente, puede enviarle cambios para que se guarden en el servidor:
user.email = '[email protected]'; await user.save(); /* or */ await user.update({ email: '[email protected]' });
También puedes eliminarlo del servidor:
await user.destroy();
Este DSL básico también se extiende a los recursos relacionados, como demostraré en el resto del tutorial. Ahora podemos aplicar rápidamente ActiveResource.js para crear el resto del CMS: publicaciones y comentarios.
Creación de publicaciones
Cree una clase de recurso para Post
y asóciela con la clase 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'); } }
Agregue Post
al índice de recursos también:
// /src/resources/index.js import Notification from './Notification'; import Post from './Post'; import User from './User'; export { Notification, Post, User };
Luego vincule el recurso Post
en un formulario para que los usuarios creen y editen publicaciones. Cuando el usuario visita por primera vez el formulario para crear una nueva publicación, se creará un recurso de Post
y cada vez que se cambie el formulario, aplicaremos el cambio a la Post
:

import Post from '/src/resources/Post'; let post = Post.build({ user: authenticatedUser }); onChange = (event) => { post.content = event.target.value; };
A continuación, agregue una devolución de llamada onSubmit
al formulario para guardar la publicación en el servidor y maneje los errores si falla el intento de guardar:
onSubmit = async () => { try { await post.save(); /* successful, redirect to edit post form */ } catch { post.errors().each((field, error) => { console.log(field, error.message) }); } }
Edición de publicaciones
Una vez que se haya guardado la publicación, se vinculará a su API como un recurso en su servidor. Puede saber si un recurso persiste en el servidor llamando a persisted
:
if (post.persisted()) { /* post is on server */ }
Para recursos persistentes, ActiveResource.js admite atributos sucios, en los que puede verificar si algún atributo de un recurso ha cambiado de su valor en el servidor.
Si llama a save()
en un recurso persistente, realizará una solicitud PATCH
que contendrá solo los cambios realizados en el recurso, en lugar de enviar el conjunto completo de atributos y relaciones del recurso al servidor innecesariamente.
Puede agregar atributos rastreados a un recurso mediante la declaración de attributes
. Hagamos un seguimiento de los cambios en post.content
:
// /src/resources/Post.js class Post extends library.Base { static define() { this.attributes('content'); /* ... */ } }
Ahora, con una publicación persistente en el servidor, podemos editar la publicación y, cuando se hace clic en el botón Enviar, guardar los cambios en el servidor. También podemos deshabilitar el botón de enviar si aún no se han realizado cambios:
onEdit = (event) => { post.content = event.target.value; } onSubmit = async () => { try { await post.save(); } catch { /* display edit errors */ } } disableSubmitButton = () => { return !post.changed(); }
Existen métodos para administrar una relación singular como post.user()
, si quisiéramos cambiar el usuario asociado con una publicación:
await post.updateUser(user);
Esto es equivalente a:
await post.update({ user });
El recurso de comentarios
Ahora cree un Comment
de clase de recurso y relaciónelo con Post
. Recuerde nuestro requisito de que los comentarios puedan ser en respuesta a una publicación u otro comentario, por lo que el recurso relevante para un comentario es 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);
Asegúrese de agregar Comment
a /src/resources/index.js
también.
También necesitaremos agregar una línea a la clase Post
:
// /src/resources/Post.js class Post extends library.Base { static define() { /* ... */ this.hasMany('replies', { as: 'resource', className: 'Comment', inverseOf: 'resource' }); } }
La opción inverseOf
que se pasa a la definición hasMany
para replies
indica que esa relación es la inversa de la definición polimórfica de belongsTo
para resource
. La propiedad inverseOf
de las relaciones se usa con frecuencia cuando se realizan operaciones entre relaciones. Por lo general, esta propiedad se determinará automáticamente a través del nombre de la clase, pero dado que las relaciones polimórficas pueden ser una de varias clases, debe definir la opción inverseOf
usted mismo para que las relaciones polimórficas tengan la misma funcionalidad que las normales.
Administrar comentarios en publicaciones
El mismo DSL que se aplica a los recursos también se aplica a la gestión de los recursos relacionados. Ahora que hemos configurado las relaciones entre las publicaciones y los comentarios, hay varias formas de administrar esta relación.
Puedes agregar un nuevo comentario a una publicación:
onSubmitComment = async (event) => { let comment = await post.replies().create({ content: event.target.value, user: user }); }
Puedes agregar una respuesta a un comentario:
onSubmitReply = async (event) => { let reply = await comment.replies().create({ content: event.target.value, user: user }); }
Puedes editar un comentario:
onEditComment = async (event) => { await comment.update({ content: event.target.value }); }
Puedes eliminar un comentario de una publicación:
onDeleteComment = async (comment) => { await post.replies().delete(comment); }
Mostrar publicaciones y comentarios
El SDK se puede usar para mostrar una lista paginada de publicaciones, y cuando se hace clic en una publicación, la publicación se carga en una nueva página con todos sus comentarios:
import { Post } from '/src/resources'; let postsPage = await Post .order({ createdAt: 'desc' }) .select('content') .perPage(10) .all();
La consulta anterior recuperará las 10 publicaciones más recientes y, para optimizarlas, el único atributo que se carga es su content
.
Si un usuario hace clic en un botón para ir a la siguiente página de publicaciones, un controlador de cambios recuperará la página siguiente. Aquí también deshabilitamos el botón si no hay páginas siguientes.
onClickNextPage = async () => { postsPage = await postsPage.nextPage(); if (!postsPage.hasNextPage()) { /* disable next page button */ } };
Cuando se hace clic en un enlace a una publicación, abrimos una nueva página cargando y mostrando la publicación con todos sus datos, incluidos sus comentarios, conocidos como respuestas, así como las respuestas a esas respuestas:
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() ); }); }
Llamar .target()
en una relación hasMany
como post.replies()
devolverá un ActiveResource.Collection
de comentarios que se cargaron y almacenaron localmente.
Esta distinción es importante, porque post.replies().target().first()
devolverá el primer comentario cargado. Por el contrario, post.replies().first()
devolverá una promesa para un comentario solicitado desde GET /api/v1/posts/:id/replies
.
También puede solicitar las respuestas de una publicación por separado de la solicitud de la publicación en sí, lo que le permite modificar su consulta. Puede encadenar modificadores como order
, select
, includes
, where
, perPage
, page
al consultar las relaciones hasMany
al igual que cuando consulta los recursos mismos.
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()); }
Modificación de recursos después de que se soliciten
A veces desea tomar los datos del servidor y modificarlos antes de usarlos. Por ejemplo, podría envolver post.createdAt
en un objeto moment()
para que pueda mostrar una fecha y hora fácil de usar para el usuario sobre cuándo se creó la publicación:
// /src/resources/Post.js import moment from 'moment'; class Post extends library.Base { static define() { /* ... */ this.afterRequest(function() { this.createdAt = moment(this.createdAt); }); } }
Inmutabilidad
Si trabaja con un sistema de administración de estado que favorece los objetos inmutables, todo el comportamiento en ActiveResource.js puede volverse inmutable configurando la 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;
Dando vueltas: vinculando el sistema de autenticación
Para concluir, le mostraré cómo integrar su sistema de autenticación de usuario en su User
ActiveResource
.
Mueva su sistema de autenticación de tokens al extremo de la API /api/v1/tokens
. Cuando el correo electrónico y la contraseña de un usuario se envían a este punto final, los datos del usuario autenticado más el token de autorización se enviarán como respuesta.
Cree una clase de recurso Token
que pertenezca 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);
Agregue el Token
a /src/resources/index.js
.
Luego, agregue un método estático de authenticate
a su clase de recursos de User
y relacione User
con el 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; } }
Este método usa resourceLibrary.interface()
, que en este caso es la interfaz JSON:API, para enviar un usuario a /api/v1/tokens
. Esto es válido: un punto final en JSON: API no requiere que los únicos tipos publicados hacia y desde él sean aquellos que le dan su nombre. Entonces la solicitud será:
{ "data": { "type": "users", "attributes": { "email": "[email protected]", "password": "password" } } }
La respuesta será el usuario autenticado con el token de autenticación incluido:
{ "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 } }] }
Luego usamos token.id
para establecer el encabezado de Authorization
de nuestra biblioteca y devolver el usuario, que es lo mismo que solicitar al usuario a través User.find()
como lo hicimos antes.
Ahora, si llama a User.authenticate(email, password)
, recibirá un usuario autenticado como respuesta y todas las solicitudes futuras se autorizarán con un token de acceso.
ActiveResource.js permite el desarrollo rápido de SDK de JavaScript
En este tutorial, exploramos las formas en que ActiveResource.js puede ayudarlo a crear rápidamente un SDK de JavaScript para administrar sus recursos de API y sus diversos recursos relacionados, a veces complicados. Puede ver todas estas características y más documentadas en el LÉAME para ActiveResource.js.
Espero que haya disfrutado de la facilidad con la que se pueden realizar estas operaciones y que use (y tal vez incluso contribuya a) mi biblioteca para sus proyectos futuros si se ajusta a sus necesidades. En el espíritu del código abierto, ¡las relaciones públicas siempre son bienvenidas!