Guía para el diseño de APIs privadas.
Todas las aplicaciones de GLA deberían permitir interactuar con el mundo exterior mediante APIs. En el caso de que la construcción sea in-house, ésta guía pretende dar un estilo de diseño común para todas ellas. La forma propuesta en la guía es la más adaptada a los estándares de la industria y en lugar de ser una propuesta novedosa es más bien un documento de desambiguación para que aquellos que tengan que diseñar APIs REST no tengan duda y en esta forma facilitar su trabajo.
Estructura de una URL
{protocolo}://{dominio o hostname}[:puerto (opcional)]/{ruta del recurso}?{query}
http://icsdesadcsrv02:19732/v8/ui/sucursales
http://localhost:8731/v8/ui/ordenesDeRetiro?sincronizar=true
Forma correcta de representar la ruta del recurso
http://itgcore/v8/{colleción}/{id}/{colleción2}/{id2}
donde {colleción} debe SIEMPRE ser un sustantivo en plural. Ejemplo:http://localhost:8731/v8/api/sucursales/13/ordenesDeEnvio/T10000000662805
Reglas básicas para ponerle nombre a la URI de un recurso
- Los nombres de URI no deben implicar una acción, por lo tanto debe evitarse usar verbos en ellos.
Forma Incorrecta: /facturas/234/editar Forma Correcta: /facturas/234.
- Deben ser independiente del formato
Forma Incorrecta: /facturas/234.pdf Forma Correcta: /facturas/234. El formato de la respuesta se espeficará en el header Accept.
- Deben mantener una jerarquía lógica
Forma Incorrecta: /facturas/234/clientes/007 Forma Correcta: /clientes/007/facturas/234
- Los filtrados de información de un recurso no se hacen en la URI
Forma Incorrecta: /facturas/orden/desc/fecha-desde/2007 Forma Correcta: /facturas?fechaDesde=2007&sort=+numero
Verbos
GET - Se usa para OBTENER o LISTAR recursos
GET /v1/api/operaciones/sucursales/3 HTTP/1.1
Host: ec2.qa.andreani.com.ar
Accept: application/xml
Cache-Control: no-cache
Devuleve una representación de la sucursal con id #3.
GET /v1/api/operaciones/sucursales HTTP/1.1
Host: ec2.qa.andreani.com.ar
Accept: application/xml
Cache-Control: no-cache
Devuelve una lista con todas las sucursales.
GET /v8/tax/paises/1/provincias/1/ciudades/ HTTP/1.1
Host: icsdesadcsrv02:19732
Authorization: Bearer wCCPzKOKUkOBfxGjkmgHzg==
Devuleve una lista con las todas las ciudades de la provincia #1 del pais #1
POST - Crear un recurso
POST /v8/api/sucursales HTTP/1.1
Host: icsdesadcsr v02.andreani.com.ar:19732
Accept: application/json
Authorization: Bearer rLecYd2DQUS8Z9IgVXHWVA==
{"nomenclatura":"STD","descripcion": "Santo Domingo"}
Crea la sucursal Santo Domingo.
Si es satisfactoria la operacion puede devolver 201-Created
o 202-Accepted
. En ambos casos debe devolver en el header Location el url que se puede invocar para obtener una representacion del recurso creado.
Location: "http://icsdesadcsrv02.andreani.com.ar:19732/v8/api/sucursales/3"
En situaciones normales los POST no deberían devolver resultados que no sean en la forma de headers.
PUT - Reemplazar un recurso
Se usa para reemplazar un recurso con uno nuevo pero manteniendo el id. Como la acción es de reemplazar, el contenido del body tiene que ser completo como si fuese un POST.
PUT /v8/api/sucursales/3 HTTP/1.1
Host: icsdesadcsr v02.andreani.com.ar:19732
Accept: application/json
Authorization: Bearer rLecYd2DQUS8Z9IgVXHWVA==
{"nomenclatura":"STDbis","descripcion":"Santo Domingo bis"}
También se puede usar para poner un recurso ya existente en una colección.
PUT /v8/api/sucursales/3/ordenesDeEnvio/W000007653 HTTP/1.1
Host: icsdesadcsr v02.andreani.com.ar:19732
Accept: application/json
Authorization: Bearer rLecYd2DQUS8Z9IgVXHWVA==
Pone la ordenDeEnvio W000007653 en la coleccion de OrdenesDeEnvio de la sucursal 3.
PATCH - Actualizar un recurso
Se usa para actualizar un recurso sin enviarle todos los datos, solo se envian aquellos que cambian.
PATCH /v8/api/sucursales/3 HTTP/1.1
Host: icsdesadcsr v02.andreani.com.ar:19732
Accept: application/json
Authorization: Bearer rLecYd2DQUS8Z9IgVXHWVA==
{"nomenclatura":"STD2"}
DELETE - Borrar un recurso
Se usa para borrar un recurso. Puede ser tambien para desactivar, dependerá el caso. Nunca es legal realizarlo sobre colecciones. Si es satisfactoria la operación devuelve 202-NoContent
.
DELETE /v8/tax/paises/1/provincias/1/ciudades/124/codigosPostales/1712 HTTP/1.1
Host: icsdesadcsrv02:19732
Authorization: Bearer wCCPzKOKUkOBfxGjkmgHzg==
Borra o desactiva el codigo postal 1712, de tal forma que un GET a /v8/tax/paises/1/provincias/1/ciudades/124/codigosPostales/1712
da 404-NotFound
.
El siguiente request es ilegal porque no se puede hacer un DELETE sobre una colección.
DELETE /v8/tax/paises/1/provincias/1/ciudades/124/codigosPostales HTTP/1.1
Host: icsdesadcsrv02:19732
Authorization: Bearer wCCPzKOKUkOBfxGjkmgHzg==
HEAD
Se usa para verificar si cierto recurso existe. En en caso afirmativo se devuelve 302-Found
y en caso contrario 404-NotFound
HEAD /v1/api/operaciones/sucursales/3 HTTP/1.1
Host: ec2.qa.andreani.com.ar
Accept: application/xml
Cache-Control: no-cache
Devuleve 302-Found
si la sucursal 3 existe.
Filtros, paginado y propiedades expandidas
Filtros y paginado
Si bien hay muchas formas de especificar valores para que sirvan de filtros en los GETs, la recomendación es usar el que sigue:
GET /v8/api/prepiezas?numeroDeEnvio=W000007653&activo=true
Host: V8QADCSRV01:19433
Authorization: Bearer nWBQecoElUO7mpoF8MHVuw==
Accept: application/json
Esa es la forma correcta: especificar en el queryString las tuplas propiedad=valor. Es conveniente que las operaciones que listen devuelvan en el response un header con el nombre x-total-count
donde informen la cantidad total de items de la colección. Eso le sirve a los controles paginadores de las UI.
Sorting
La clave sort del queryString indica sobre qué campo y en qué sentido se debe hacer el ordenamiento.
GET /v8/api/prepiezas/?numeroDeEnvio=W000007653&activo=true&sort=-fechaDeEntrada
Host: V8QADCSRV01:19433
Authorization: Bearer nWBQecoElUO7mpoF8MHVuw==
Accept: application/json
Un signo menos (-) indica que se debe ordenar de forma descendente.
Paginación
GET /v8/api/prepiezas?numeroDeEnvio=W000007653&activo=true&sort=-fechaDeEntrada&offest=2&limit=50
Host: V8QADCSRV01:19433
Authorization: Bearer nWBQecoElUO7mpoF8MHVuw==
Accept: application/json
Donde offest indica el número de página que hay que mostrar y limit la cantidad de items.
Propiedades expandidas
En el ejemplo anterior hay una clave del query string que se llaman expand, ¿a qué viene esto?: en general una entidad tiene relaciones con otras, por ejemplo, una ordenDeEnvio estará relacionada con una cuenta corriente o un contrato y entonces cuando devolvemos la información de la orden de envío, ¿debemos devolver también la representación de sus asociaciones?. La primera respuesta es no, y lo que sí se debe devolver es el url que el cliente tiene que invocar en el caso de que requiera el valor de esa relación:
[
{
"componentes": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/prePiezas/{cabecera:676474,numero:1}/componentes"
},
"contrato": {
"esCorporativo": false,
"esRecanalizable": false,
"esUrgente": false,
"estaActivo": false,
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/contratos/32050/iditg"
},
"cpDestinoValidado": {
"ciudad": "589",
"codigo": 1834,
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/direcciones/paises/1/provincias/1/ciudades/589/codigosPostales/1834",
"idCiudad": 589,
"idPais": 1,
"idProvincia": 1
},
"destinatario": {
"apellido": null,
"documento": "DNI 23402292",
"email": "timmothy_breitenberg@powlowskigusikowski.co.uk",
"nombre": "Stefan",
"nombreCompleto": "Stefan DNI 23402292",
"telefonos": [],
"tipoDocumento": null
},
"destinoInformado": {
"calle": "santo domingo",
"ciudad": "TEMPERLEY",
"codigoPostal": "1834",
"numero": "40",
"pais": "Argentina",
"provincia": "AR"
},
"fechaDeEntrada": "2017-11-14T12:13:11.0000000",
"lote": "201711141213111422",
"numero": 1,
"numeroDeEnvio": "W00000000375650",
"remitente": {
"apellido": null,
"documento": null,
"email": "annamae@goldner.ca",
"nombre": "Reuben",
"nombreCompleto": "Reuben ",
"telefonos": [],
"tipoDocumento": null
},
"subLote": "1",
"sucursalDeDistribucion": 9,
"sucursalDeImposicion": 0,
"valorACobrar": 0,
"valorDeclarado": 0
}
]
Notar en este ejemplo el valor de la propiedad contrato. Tiene un miembro de nombre href que indica el url de dónde obtener ese contrato. Lo mismo pasa con la propiedad cpDestinoValidado. Sin embargo, pueden haber casos en que si sea conveniente obtener en el mismo request las asociaciones del recurso y ahí entran las propiedades expandidas. La clave expand especifica en su valor el nombre de la relación que se quiere incluir en la respuesta:
GET /v8/api/prepiezas?expand=contrato HTTP/1.1
[
{
"componentes": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/prePiezas/{cabecera:676474, numero:1 }/componentes"
},
"contrato": {
"cicloDeDistribucion": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/contratos/ciclos/746/fases",
"permiteEntregaEnMostrador": null,
"permiteReenvio": false
},
"cicloDeReenvio": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/contratos/ciclos/746/fases",
"permiteEntregaEnMostrador": null,
"permiteReenvio": false
},
"cicloDeRendicionParaEntrega": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/contratos/ciclos/227/fases",
"permiteEntregaEnMostrador": null,
"permiteReenvio": false
},
"cicloDeRendicionParaNoEntrega": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/contratos/ciclos/227/fases",
"permiteEntregaEnMostrador": null,
"permiteReenvio": false
},
"cliente": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/clientes/36184/iditg"
},
"codigoDeContratoInterno": "400006611",
"descripcion": "45440-26024-ENT-AO",
"direccionPostal": {
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/direcciones/32818598?esDeConfiguracion=True",
"idLocalidad": null,
"idPais": null,
"idProvincia": null,
"iditg": null
},
"esCorporativo": true,
"esRecanalizable": false,
"esUrgente": false,
"estaActivo": true,
"iditg": "32050",
"tipoDeServicio": "Encomienda eCommerce"
},
"cpDestinoValidado": {
"ciudad": "589",
"codigo": 1834,
"href": "http://icstestdcsrv02.andreani.com.ar:19732/v8/api/direcciones/paises/1/provincias/1/ciudades/589/codigosPostales/1834",
"idCiudad": 589,
"idPais": 1,
"idProvincia": 1
},
"destinatario": {
"apellido": null,
"documento":"DNI 23402292",
"email": "timmothy_breitenberg@powlowskigusikowski.co.uk",
"nombre": "Stefan",
"nombreCompleto":"Stefan DNI 23402292",
"telefonos": [],
"tipoDocumento": null
},
"destinoInformado": {
"calle": "santo domingo",
"ciudad": "TEMPERLEY",
"codigoPostal": "1834",
"numero": "40",
"pais": "Argentina",
"provincia": "AR-B"
},
"fechaDeEntrada": "2017-11-14T12:13:11.0000000",
"lote": "201711141213111422",
"numero": 1,
"numeroDeEnvio": "W00000000375650",
"remitente": {
"apellido": null,
"documento": null,
"email": "annamae@goldner.ca",
"nombre": "Reuben",
"nombreCompleto": "Reuben ",
"telefonos": [],
"tipoDocumento": null
},
"subLote": "1",
"sucursalDeDistribucion": 9,
"sucursalDeImposicion": 0,
"valorACobrar": 0,
"valorDeclarado": 0
}
]
La respuesta incluye la información del contrato. Es legal solicitar más de una asociación en el expand, por ejemplo
GET /v8/api/prepiezas?expand=contrato,cpDestinoValidado
es totalmente correcto y el API deberá devolver ambas asociaciones inicializadas.
El uso de las propiedades expandidas no es obligatorio y es decisión del diseñador del API si es plausible de implementar o no. En general lo será si implica un costo mucho menor del que hacer otro GET separado.
Headers
Accept : Todas las apis de Andreani deben hacer caso de este header y devolver el mime-type que corresponda.
Cache-Control : expiry:
etag if-match
Supongamos el siguiente request:
GET /v1/api/operaciones/sucursales/3 HTTP/1.1
Host: ec2.qa.andreani.com.ar
Accept: application/xml
Cache-Control: no-cache
Etag: 1234
En etag viene un hash que representa la versión del recurso. La forma del hash es dependiente de la implementación. Si luego un cliente quiere actualizar o borrar ese recurso debería incluir en el request un header if-match:
PUT /v8/api/sucursales/3 HTTP/1.1
Host: icsdesadcsr v02.andreani.com.ar:19732
Accept: application/json
Authorization: Bearer rLecYd2DQUS8Z9IgVXHWVA==
If-Match: 1234
{"nomenclatura":"STDbis","descripcion":"Santo Domingo bis" }
Si el if-match coincide con el etag, entonces el recurso se puede actualizar. Si no es así la implementación debería devolver 412 Precondition Fails