Consejos de diseño de API RESTful de la experiencia

Una guía de trabajo de consejos de diseño de API y evaluaciones de tendencias.

© Nathaniel Merz - Picos imaginarios vía. 500px
  • ¡He migrado las últimas versiones de este artículo a mi GitHub!
  • https://github.com/ptboyer/restful-api-design-tips
  • ️ ¡No dudes en leerlo allí y dejar una estrella si lo disfrutaste!
  • Última actualización en Medium, 9 de junio de 2019.
Todos somos aprendices en un oficio donde nadie se convierte en maestro.

Mientras escribo esto, me río para mí mismo al ver un gran paralelismo detrás de mí mismo haciendo referencia a la cita de Hemingway de otra persona; ¡La simple idea de que no necesito esforzarme para crear una implementación diferente del pasaje con una funcionalidad similar para el valor del resultado (o en este caso, el significado) es un testamento literario para la reutilización del código!

Pero no estoy aquí para escribir sobre los beneficios de los paquetes de código, sino para mencionar algunos de los rasgos que he llegado a apreciar e implementar activamente en proyectos presentes y futuros. Y de estas características y detalles de implementación, cultivo mi propio paquete de reglas API y primitivas.

Desde la publicación de este artículo, muchos hilos de discusión en canales como Reddit me han ayudado a ajustar y ajustar algunas de mis explicaciones y posturas sobre el diseño de API. ¡Quisiera agradecer a todos los que han contribuido a la discusión, y espero que esto ayude a convertir este artículo en un recurso más valioso para otros! (Edición: 9 / junio / 2019) Y ahora han pasado dos años desde que publiqué este artículo por primera vez y ha sido increíble ver que se ha visto más de 150,000 veces y recibido miles de Me gusta y acciones, y una vez más quiero expresar mi gratitud a todos mis lectores y seguidores!

Versionado

Si va a desarrollar una API para cualquier servicio al cliente, querrá prepararse para un eventual cambio. Esto se logra mejor al proporcionar un "espacio de nombres de versión" para su API RESTful.

Hacemos esto simplemente agregando la versión como prefijo a todas las URL.

OBTENGA www.myservice.com/api/v1/posts

Sin embargo, a través del estudio de otras implementaciones de API, me ha gustado un estilo de URL más corto que se ofrece accediendo a la API como parte de un subdominio y luego eliminando / api de la ruta; más corto y más conciso es mejor.

OBTENGA api.myservice.com/v1/posts

Intercambio de recursos de origen cruzado (CORS)

Es importante tener en cuenta que al colocar su API en un subdominio diferente, como api.myservice.com, será necesario implementar CORS para su backend si planea alojar su sitio frontend en www.myservice.com y espera utilizar solicitudes de búsqueda sin arrojar el encabezado No Access-Control-Allow-Origin es errores presentes.

Rutas

Al construir sus rutas, debe pensar en sus puntos finales como grupos de recursos desde los que puede leer, agregar, editar y eliminar, y estas acciones se encapsulan como métodos HTTP.

Usar métodos HTTP

Use métodos como:

  • OBTENGA para obtener datos.
  • POST para agregar datos.
  • PUT para actualizar datos (como un objeto completo).
  • PARCHE para actualizar datos (con información parcial del objeto).
  • BORRAR para borrar datos.

Me gustaría agregar que creo que PATCH es una excelente manera de reducir el tamaño de las solicitudes para cambiar partes de objetos más grandes, pero también que encaja bien con los campos de envío automático / guardado automático implementados comúnmente.

Un buen ejemplo de esto es con la pantalla "Configuración del tablero" de Tumblr, donde las opciones no críticas sobre la experiencia del usuario del servicio se pueden editar y guardar, por elemento, sin la necesidad de un botón de envío de formulario final. Es simplemente una forma mucho más orgánica de interactuar con los datos de preferencias del usuario.

La etiqueta "Guardado" aparece y luego desaparece poco después de modificar la opción.

Usar plurales

Tiene sentido semántico cuando solicita muchas publicaciones / publicaciones.

Y por amor de Dios, ¡no consideres / post / all con / post /: id!

// DO: los plurales son consistentes y tienen sentido
GET / v1 / posts /: id / attachments /: id / comments

// NO: ¿es solo un comentario? es una forma? etc.
GET / v1 / post /: id / attach /: id / comment

¡En casos como estos, simplemente debe intentar acercarse lo más posible al plural!

"Me gusta la idea de usar plurales para nombres de recursos, pero a veces se obtienen nombres que no se pueden pluralizar". (Fuente)

Usar anidamiento para el filtrado de relaciones

Las cadenas de consulta deben usarse para filtrar aún más los resultados más allá de la agrupación inicial de un conjunto lógico ofrecido por una relación.

Apunte a diseñar rutas de punto final que eviten parámetros de cadena de consulta innecesarios, ya que generalmente son más difíciles de leer y trabajar en comparación con las rutas cuya estructura promueve un filtrado y agrupación inicial basada en relaciones de dichos elementos a mayor profundidad.

Esto / posts / x / adjuntos es mejor que / adjuntos? PostId = x. Y esto / posts / x / attachments / y / comments es mucho mejor que / comments? PostId = x & attachId = y.

Utilice más de su "ruta-espacio"

Debes intentar mantener tu API lo más plana posible y no saturar tus rutas de recursos. Permítase proporcionar rutas planas para actualizar / eliminar sus recursos, como en el caso de publicaciones que tengan comentarios, permita que / posts /: id / comments obtenga los comentarios para una publicación basada en la relación, pero también ofrezca / comments /: id para permitir la edición de comentarios sin necesidad de un identificador para la publicación de cada ruta.

  • Rutas más largas para crear / recuperar recursos anidados por relación
  • Rutas más cortas para actualizar / eliminar recursos en función de su identificación.

Use el contexto de autorización para filtrar

Cuando se trata de proporcionar un punto final para acceder a todos los recursos propios de un usuario (por ejemplo, todas mis publicaciones), puede encontrar muchas maneras de proporcionar esa información, depende de usted lo que mejor se adapte a su aplicación.

  1. Anide una relación / posts debajo de / me con GET / me / posts, o
  2. Utilice el punto final existente / posts pero filtre con una cadena de consulta, GET / posts? User = , o
  3. Reutilice / publicaciones para mostrar solo sus propias publicaciones y exponga publicaciones públicas con GET / feed / posts.

Utilice un punto final "Yo"

Tenga un punto final como GET / me para entregar datos básicos sobre el usuario, distinguidos a través del encabezado de Autorización. Esto puede incluir información sobre los permisos / ámbitos / grupos / publicaciones / sesiones del usuario, etc., que permiten al cliente mostrar / ocultar elementos y rutas en función de sus permisos.

Cuando se trata de proporcionar puntos finales para actualizar las preferencias del usuario, permite que PATCH / me cambie esos valores intrínsecos.

Proporcionar paginación

La paginación es realmente importante porque no desea que una solicitud simple sea increíblemente costosa si hay miles de filas de resultados. Parece obvio, pero muchos descuidan esta funcionalidad.

Hay varias formas de hacer esto:

Parámetro "De"

Podría decirse que es el más fácil de implementar, donde la API acepta un parámetro de cadena de consulta y luego devuelve un número limitado de resultados de ese desplazamiento (comúnmente 20 resultados).

También es mejor proporcionar un parámetro de límite que tenga un máximo rígido, como el caso de Twitter, con un máximo de 1000 y un límite predeterminado de 200.

Token de página siguiente

La API de Google Places devuelve un next_page_token en sus respuestas si hay más información disponible más allá de los 20 resultados limitados por página. Luego acepta pagetoken como parámetro para una nueva solicitud que continúa devolviendo más resultados con un nuevo next_page_token hasta que se agota. Twitter hace algo similar en lugar de usar un parámetro llamado next_cursor.

Respuestas

Usar sobres

“No me gustan los datos envolventes. Simplemente introduce otra clave para navegar por un árbol de datos potencialmente denso. La metainformación debe ir en encabezados ".
“Un argumento para anidar datos es proporcionar dos claves raíz distintas para indicar el éxito de la respuesta, * datos * y * error *. Sin embargo, delego esta distinción a los códigos de estado HTTP en caso de errores ".

Originalmente, sostuve la postura de que los datos envolventes no son necesarios, y que HTTP proporcionó un "sobre" adecuado en sí mismo para entregar una respuesta. Sin embargo, después de leer las respuestas en Reddit, pueden producirse varias vulnerabilidades y posibles hacks si no se envuelven las matrices JSON.

¡Deberías envolver tus respuestas de datos!

// DO: envuelto
{
  datos: [
    {...}
    {...}
    // ...
  ]
}

// NO envuelto
[
  {...}
  {...}
  // ...
]
"Además, si desea utilizar una herramienta como normalizr para analizar datos de las respuestas del lado del cliente, eliminar un sobre elimina la necesidad de extraer constantemente los datos de la carga útil de respuesta para pasarlos a la normalización".

Por el contrario, proporcionar una clave adicional para acceder a sus datos permite verificar de manera confiable si realmente se devolvió algo, y si no, puede referirse a una clave de error que no colisiona separada del cuerpo de una respuesta.

¡También es importante tener en cuenta que, a diferencia de algunos, lenguajes como JavaScript evaluarán los objetos vacíos como verdaderos! Por lo tanto, es importante no devolver un objeto vacío por error como parte de una respuesta en el caso de:

// envuelto, extracción de error de la carga útil
const {datos, error} = carga útil
// procesar errores si existen
if (error) {tirar ...}
// de lo contrario
const normalizedData = normalize (datos, esquema)

JSON respuestas y solicitudes

“Todo debería ser serializado en JSON. Si espera JSON del servidor, sea cortés y proporcione JSON al servidor. ¡Consistencia!"

Obviamente, "todo" es una exageración como algunos comentarios señalan, pero tenía la intención de referirse a cualquier objeto simple y simple que debería ser serializado para el proceso de consumo y / o regreso de la API.

Es esencial definir sus tipos de medios a través de encabezados en las respuestas y solicitudes de una API RESTful. Al tratar con JSON, asegúrese de incluir un Tipo de contenido: encabezado de aplicación / json y, respectivamente, para otros tipos de respuesta, ya sean CSV o binarios.

Devolver el objeto actualizado

¡Al actualizar cualquier recurso a través de un PUT o PATCH, es una buena práctica devolver el recurso actualizado en respuesta a una solicitud exitosa de POST, PUT o PATCH!

Use 204 para eliminaciones

Ha habido casos en los que no he tenido nada que devolver del éxito de una acción (es decir, ELIMINAR), sin embargo, siento que devolver un objeto vacío puede ser evaluado como falso en algunos lenguajes (como Python) y puede no ser tan obvio a un humano depurando su aplicación.

Admite el código de estado 204 - Sin contenido de respuesta en casos donde la solicitud fue exitosa pero no tiene contenido para devolver. El sobre de la respuesta, junto con un código de éxito HTTP 2XX es suficiente para indicar una respuesta exitosa sin "información" arbitraria.

BORRAR / v1 / posts /: id
// respuesta - 204
{
  "datos": nulo
}

Usar códigos de estado HTTP y respuestas de error

Debido a que estamos usando métodos HTTP, debemos usar códigos de estado HTTP. Aunque un desafío aquí es seleccionar una porción distinta de estos códigos, y luego depender de los datos de respuesta para detallar cualquier error de respuesta. Mantener un pequeño conjunto de códigos lo ayuda a consumir y manejar los errores de manera consistente.

Me gusta usar:

para errores de datos

  • 400 para cuando la información solicitada esté incompleta o mal formada.
  • 422 para cuando la información solicitada está bien, pero no es válida.
  • 404 para cuando todo esté bien, pero el recurso no existe.
  • 409 para cuando existe un conflicto de datos, incluso con información válida.

para errores de autenticación

  • 401 para cuando un token de acceso no se proporciona o no es válido.
  • 403 para cuando un token de acceso es válido, pero requiere más privilegios.

para estados estándar

  • 200 para cuando todo esté bien.
  • 204 para cuando todo esté bien, pero no hay contenido para devolver.
  • 500 para cuando el servidor arroja un error, completamente inesperado.

Además, devolver respuestas después de estos errores también es muy importante. Quiero considerar no solo la presentación del estado en sí, sino también una razón detrás de esto.

En el caso de intentar crear una nueva cuenta, imagine que proporcionamos un correo electrónico y una contraseña. Por supuesto, nos gustaría que nuestra aplicación cliente evite cualquier solicitud con un correo electrónico no válido o una contraseña que sea demasiado corta, pero los externos tienen tanto acceso a la API como nosotros desde nuestra aplicación cliente cuando está activa.

  • Si falta el campo de correo electrónico, devuelva un 400.
  • Si el campo de contraseña es demasiado corto, devuelva un 422.
  • Si el campo de correo electrónico no es un correo electrónico válido, devuelva un 422.
  • Si el correo electrónico ya está en uso, devuelva un 409.
"Es mucho mejor especificar un código de la serie 4xx más específico que simplemente 400. Entiendo que puede poner lo que quiera en el cuerpo de respuesta para desglosar el error, pero los códigos son mucho más fáciles de leer de un vistazo". (Fuente)

Ahora, a partir de estos casos, dos errores devolvieron 422 independientemente de que sus razones sean diferentes. Es por eso que necesitamos un código de error, y tal vez incluso una descripción del error. Es importante hacer una distinción entre el código y la descripción, ya que tengo la intención de tener el código como una constante consumible de la máquina, y el mensaje como una cadena consumible humana que puede cambiar.

En el caso de errores por campo, la presencia del campo como clave en el error es suficiente de un "código" para indicar que es el objetivo de un error de validación.

Errores de validación de campo

Para devolver los errores por campo, puede devolverse como:

POST / v1 / registrarse
// solicitud
{
  "correo electrónico": "end @@ user.comx"
  "contraseña": "abc"
}
// respuesta - 422
{
  "error": {
    "código": "FIELDS_VALIDATION_ERROR",
    "mensaje": "Uno o más campos generaron errores de validación".
    "campos": {
      "email": "Dirección de correo electrónico no válida",
      "contraseña": "Contraseña demasiado corta".
    }
  }
}

Errores de validación operacional

Y para devolver errores de validación operativa:

POST / v1 / registrarse
// solicitud
{
  "correo electrónico": "end@user.com",
  "contraseña": "contraseña"
}
// respuesta - 409
{
  "error": {
    "código": "EMAIL_ALREADY_EXISTS",
    "mensaje": "Ya existe una cuenta con este correo electrónico".
  }
}

El mensaje puede actuar como un mensaje de error de lectura humana alternativo para ayudar a comprender la solicitud durante el desarrollo, y también en el caso de que no se pueda utilizar una implementación de cadena de localización adecuada.

De esta manera, su lógica de búsqueda vigila los errores que no son 200, y luego puede verificar directamente la clave de error de la respuesta y luego compararla con cualquier otra lógica en la aplicación cliente.

Autenticación

Las API RESTful sin estado modernas implementan la autenticación con tokens que se proporcionan más comúnmente a través del encabezado de autorización (o incluso un parámetro de consulta access_token).

Use tokens de sesión de extensión automática

Originalmente pensé que emitir JWT para solicitudes API regulares era una excelente manera de manejar la autenticación, hasta que quería invalidar esos tokens.

En mi última revisión de esta publicación (y detallada en una publicación separada) ofrecí una forma de volver a emitir los JWT a través de un "token de actualización" secreto (RT) de cliente almacenado adicionalmente que se cambiaría por nuevos JWT. Sin embargo, para expirar estos JWT, cada uno de ellos contenía una referencia al RT emisor, por lo que si el RT se invalidaba / eliminaba, también lo haría el JWT. Sin embargo, este mecanismo derrota la apatridia del propio JWT ...

Mi solución ahora es simplemente usar un punto final de recursos / sessions para intercambiar credenciales de inicio de sesión por un único token de sesión único (usando uuid4) que se agrupa y almacena como una fila de base de datos. Al igual que muchas aplicaciones modernas, no es necesario volver a emitir el token a menos que haya un largo período de inactividad (similar al tiempo de espera de la sesión, pero a la escala de semanas). Después de la autenticación inicial, cada solicitud futura afecta la vida útil del token de manera que se extienda automáticamente siempre que no haya expirado.

Creación de sesión: inicio de sesión

Un proceso de inicio de sesión normal se vería así:

  1. Reciba combinación de correo electrónico / contraseña con POST / sesiones, tratando las sesiones como un recurso más.
  2. Comprueba el correo electrónico / hash de contraseña en la base de datos.
  3. Cree una nueva fila de base de datos de sesión que contenga un hash uuid4 () como token.
  4. Devuelva la cadena de token no hash al cliente.

Renovación de sesión

En este flujo, los tokens no necesitan ser explícitamente renovados o reeditados. Esto se debe a que la API extiende la vida útil del token si aún es válida en todas las solicitudes, lo que evita que los usuarios habituales venzan una sesión.

Cada vez que la API recibe un token, es decir, a través de un encabezado de autorización:

  1. Reciba el token, es decir, del encabezado de autorización.
  2. Compare con el hash del token, si no hay una fila de sesión coincidente, genere un error de autenticación.
  3. Verifique la propiedad updated_at de la sesión, si ahora si es mayor que updated_at + session_life la sesión se considera expirada, elimine la fila de la sesión, genere un error de autenticación.
  4. Si existe y sigue siendo válido desde updated_at time, establezca updated_at time en now () para renovar el token.

Manejo de sesiones

Debido a que todas las sesiones se rastrean como filas de la base de datos asignadas a un usuario, un usuario puede ver todas sus sesiones activas de manera similar a la vista de sesiones de seguridad de la cuenta de Facebook. También puede optar por incluir los metadatos asociados que haya elegido recopilar al crear inicialmente una sesión, como el Agente de usuario del navegador, la dirección IP, etc.

Obtener todas sus sesiones es tan simple como:

  1. GET / sessions para devolver todas las sesiones asociadas con su usuario a través del encabezado Autorización.

Terminación de la sesión: cierre de sesión

Y debido a que tiene identificadores para sus sesiones, puede cancelarlos para invalidar el acceso no autorizado o no deseado a su cuenta. Y cerrar sesión sería simplemente terminar la sesión del cliente y purgar la sesión del cliente.

  1. Reciba el token como parte de una solicitud DELETE / sessions / id.
  2. Compare con el hash del token, elimine la fila de sesión correspondiente.

Evite las reglas de composición de contraseña

Después de investigar mucho sobre las reglas de contraseña, he llegado a un acuerdo sobre que las reglas de contraseña son una mierda y son parte de los "no" de NIST, especialmente teniendo en cuenta que las reglas de composición de contraseña ayudan a reducir las contraseñas válidas en función de sus reglas de validez.

He recopilado algunos de los mejores puntos (de los enlaces anteriores) para el manejo de contraseñas:

  1. Solo aplique una longitud mínima de contraseña Unicode (mínimo 8-10).
  2. Verificar contraseñas comunes ("contraseña12345")
  3. Verifique la entropía básica (no permita "aaaaaaaaaaaaa").
  4. No utilice reglas de composición de contraseña (al menos un "! @ # $% &").
  5. No utilice sugerencias de contraseña (rima con "assword").
  6. No utilice la autenticación basada en el conocimiento (preguntas de "seguridad").
  7. No caduque las contraseñas sin motivo.
  8. No use SMS para la autenticación de dos factores.
  9. Use una contraseña de sal de 32 bits o más.

¡Estos "no hacer" deberían facilitar la validación de la contraseña!

Meta

Utilice un punto final de "Health Check"

Mediante el desarrollo con AWS, ha sido necesario proporcionar una forma de generar una respuesta simple que pueda indicar que la instancia de API está activa y no necesita reiniciarse. También es útil para verificar fácilmente qué versión de la API está en cualquier máquina en cualquier momento, sin autenticación.

OBTENER / v1
// respuesta - 200
{
  "estado": "en ejecución",
  "versión": "fdb1d5e"
}

Proporciono el estado y la versión (que se refiere a la referencia git commit de la API en el momento en que se creó). También vale la pena mencionar que este valor no se deriva de un repositorio .git activo que se incluye con el contenedor de API para EC2. En cambio, se lee (y se almacena en la memoria) en la inicialización desde un archivo version.txt (que se genera a partir del proceso de compilación), y por defecto es __UNKNOWN__ en caso de un error de lectura, o el archivo no existe.

¡Gracias por leer!

Si disfrutaste mi artículo y / o lo encontraste útil, agradecería que dejes un aplauso o dos aquí en Medium, y destaques mi artículo en GitHub ️.

Siéntase en la libertad de dejar un comentario; ¡tengamos una conversación!