¿Qué es un API Rest?
Lo primero que debemos de ver son los conceptos:
API
Imagina que para manejar un auto no cuentas con:
Un volante
Una palanca de velocidades
Tablero
Te imaginas tooodo lo que tendrías que hacer para poder echar a andar el auto, mover las llantas, para prender las direcciones conectar el cable, todo sería mucho más complicado. En qué nos ayuda el volante, la palanca de velocidades y el tablero?
Realmente el estos elementos saben como manipular las partes del auto para poder dar vuelta, cambiar la velocidad ó encender las luces y lo hacen de una manera que simplemente nosotros no necesitamos conocer a fondo como funciona el auto.
El trabajo de un API es similar, las empresas crean sus APIs donde ellos tienen toda la lógica de su negocio, y hay “clientes” que son quienes implementan ó consumen el API, que simplemente piden información a esta API y el API sin necesidad de que el cliente conozca toda la lógica para obtener esa información.
Uno de los ejemplo es el API de Facebook, donde se exponen varias funcionalidades de facebook en unas peticiones HTTP tipo REST.
REST
REST básicamente significa Representational State Transfer (Transferencia de Estado Representacional), no te apures a mi tampoco hasta la fecha me dice mucho su nombre, pero ahora lo vemos más simple.
Básicamente es un ESTILO de arquitectura, no es una ley ni un protocolo simplemente son consejos ó ideas que al implementarlas en nuestro server nos dictan que sigue un estilo REST, y estás son las carácteristicas de este estilo:
Pongamos el siguiente ejemplo:
- Recursos
- CRUD
- Formato
Resources (recursos)
En el ejemplo anterior, cuáles son los recursos que se utilizan:
Podrían ser:
- Ropa: La ropa la crea el dueño de la tienda, le asigna una imagen, talla, color, código y un precio.
- Usuarios: Los usuarios registrados en tu base de datos
- Ordenes de pago: El cliente puede al hacer una compra pagar por una prenda ó varias, al crearlas los datos que podría llevar son:
- código de la prenda (SKU)
- cantidad
- monto por prenda
- monto total
- Dirección de envío
- Envíos: La dirección del envío que cada usuario puede tener una ó mas.
Para cada recurso de tu API en el estilo REST se tiene un path es decir tu URL:
- https://<tuURL>/ropa
- https://<tuURL>/usuarios
- https://<tuURL>/usuario/<id>/pagos
- https://<tuURL>/usuario/<id>/direcciones
Como puedes ver hay una diferente:
- https://<tuURL>/usuario/<id>/direcciones
- https://<tuURL>/usuario/<id>/pagos
Y es que direcciones y pagos tienen dueño, corresponden a un usuario, si bien
- https://<tuURL>/usuarios
Te deja consultar todos los usuarios, esto:
- https://<tuURL>/usuario/<id>
Te dejara consultar solo un usuario y esto:
- https://<tuURL>/usuario/<id>/<nombre del recurso>
Todos los recursos asociados a un usuario. Entonces:
- https://<tuURL>/usuario/1005/pagos
Tendría todos los pagos del usuario 1005
CRUD (Create-Update-Delete)
Cada recurso, por ejemplo en este caso ropa, puede CREARSE, ACTUALIZARSE y ELIMINARSE en la base de datos.
Para esto utilizamos los métodos del protocolo HTTP.
El protocolo HTTP es parte de nuestro día a día, cada que consultamos una página de internet en nuestro navegador utilizamos el protocolo HTTP y el método GET, no me crees?
Abre tu terminal y ejecuta el comando
curl www.google.com con esto ejecutaremos el protocolo HTTP y el método get de la url de google.
verás el montón de código, que arroja la terminal, lo reconoces? ES HTML!! y no es que una página sea REST, lo que pasa es que el método GET del protocolo HTTP regresa un archivo HTML para esté caso
El protocolo HTTP y sus métodos en un estilo REST deben de utilizarse de la siguiente manera
Aún hay más pero estos son los más básicos:
- POST para crear recursos
- GET para consulta de recursos
- PUT para actualizar TODOS los atributos de un recurso
- PATCH para actualizar un recurso parcialmente, ósea no todos sus atributos.
- DELETE para borrar recursos.
Como es un estilo no es ley que se utilicen así pero la gran mayoría lo utiliza así, algunos utilizan PUT para crear recursos en lugar de POST. Depende de tu diseño.
Formato
Si bien al ejecutar el comando
curl www.google.com
recibimos un archivo HTML, el protocolo HTTP puede regresar muchos formatos, video, música, pdf, html, json, xml, etc.
Para el estilo REST debe de regresar XML ó JSON algo así:
<persona id=1> | |
<nombre>Pepito Perez</nombre> | |
<telefono>5512345678</telefono> | |
</persona> |
{ | |
"id": 1, | |
"nombre": "Pepito Perez", | |
"telefono": 5512345678 | |
} |
Ambos representan el recurso persona, pero como puedes ver el formato es distinto, depende también para que lo utilices y cuál sea tú diseño.
Repaso antes de lo práctico
En general el estilo REST define que debemos crear recursos en forma de paths en nuestra URL, y usar los métodos HTTP generalmente POST, PUT, PATCH, GET y DELETE para crear CRUDs con formato JSON ó XML.
Ejemplo:
recurso: Ropa
path: https://<URL>/ropa
Consulta
Todas las ropas:
GET
https://<URL>/ropa
Una Ropa por identificador
GET
https://<URL>/ropa/<ID>
CRUD:
Crear Ropa (POST):
https://<URL>/ropa
{ body (JSON) }
Editar Ropa (PUT):
https://<URL>/ropa/<ID>
{ body (JSON) }
Eliminar Ropa (DELETE):
https://<URL>/ropa/<ID>
Práctico!
Para este ejemplo vamos a usar Spring Boot con kotlin y crearemos un API de personas, para este ejemplo NO HABRÁ bases de datos eso vendrá después.
Creando el proyecto
Podemos usar como IDE: Eclipse, STS ó IntelliJ en mi caso yo utilizo IntelliJ pero son los mismos pasos:
Cuando termines de crear el proyecto deberías de poder ver un pom.xml
Debe de verse más ó menos así:
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<modelVersion>4.0.0</modelVersion> | |
<parent> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-parent</artifactId> | |
<version>2.3.3.RELEASE</version> | |
<relativePath/> <!-- lookup parent from repository --> | |
</parent> | |
<groupId>com.codetecuhtli</groupId> | |
<artifactId>apirest</artifactId> | |
<version>0.0.1</version> | |
<name>apirest</name> | |
<description>Demo project for Spring Boot</description> | |
<properties> | |
<java.version>1.8</java.version> | |
<kotlin.version>1.3.72</kotlin.version> | |
<spring-cloud.version>Hoxton.SR7</spring-cloud.version> | |
</properties> | |
<dependencies> | |
<dependency> | |
<groupId>org.jetbrains.kotlin</groupId> | |
<artifactId>kotlin-reflect</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.jetbrains.kotlin</groupId> | |
<artifactId>kotlin-stdlib-jdk8</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.cloud</groupId> | |
<artifactId>spring-cloud-starter</artifactId> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-devtools</artifactId> | |
<scope>runtime</scope> | |
<optional>true</optional> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-test</artifactId> | |
<scope>test</scope> | |
<exclusions> | |
<exclusion> | |
<groupId>org.junit.vintage</groupId> | |
<artifactId>junit-vintage-engine</artifactId> | |
</exclusion> | |
</exclusions> | |
</dependency> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web-services</artifactId> | |
</dependency> | |
</dependencies> | |
<dependencyManagement> | |
<dependencies> | |
<dependency> | |
<groupId>org.springframework.cloud</groupId> | |
<artifactId>spring-cloud-dependencies</artifactId> | |
<version>${spring-cloud.version}</version> | |
<type>pom</type> | |
<scope>import</scope> | |
</dependency> | |
</dependencies> | |
</dependencyManagement> | |
<build> | |
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> | |
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> | |
<plugins> | |
<plugin> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-maven-plugin</artifactId> | |
</plugin> | |
<plugin> | |
<groupId>org.jetbrains.kotlin</groupId> | |
<artifactId>kotlin-maven-plugin</artifactId> | |
<configuration> | |
<args> | |
<arg>-Xjsr305=strict</arg> | |
</args> | |
<compilerPlugins> | |
<plugin>spring</plugin> | |
</compilerPlugins> | |
</configuration> | |
<dependencies> | |
<dependency> | |
<groupId>org.jetbrains.kotlin</groupId> | |
<artifactId>kotlin-maven-allopen</artifactId> | |
<version>${kotlin.version}</version> | |
</dependency> | |
</dependencies> | |
</plugin> | |
</plugins> | |
</build> | |
</project> |
Agrega esta dependencia a tu pom file de esta manera
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web-services</artifactId> | |
</dependency> |
Y compila para que puedas usar las dependencias.
Componentes Spring
Al crear web services en spring generalmente hay buenas prácticas que seguir, y estás con usar:
- @Repository: Anotar una clase con @Repository, indicamos que esa clase tendrá la responsabilidad de guardar y obtener datos para el service, puede ser la conexión a una base de datos, llamadas a otros web services, etc.
- @Service: Anotar una clase con @Service implica que esa clase puede inyectar @Repository en ella, un service en Spring tiene toda la lógica de negocio, llama a los repositories para obtener sus datos y hace operaciones con ellos a fin de que esa respuesta sea la final para el servicio web. Al service no le interesa de dónde saca @Repository al final, solamente los pide y los procesa.
- @RestController: el @RestController tiene al service y es el encargado de exponer la lógica al cliente, aquí definimos los paths de la url y que método HTTP corresponde a que función del service.
Creando el @Repository
Como comentábamos el repositorio se encarga de obtener los datos, generalmente es una base de datos a la que se conecta ó un servicio web, contiene toda la lógica para obtener los datos de cualquier fuente.
@Repository | |
class PersonRepository { | |
companion object { | |
val people = mutableListOf( | |
Person(1,"Pepe", "5512345678"), | |
Person(2, "Maria", "5522334455"), | |
Person(3, "Beto", "5566774411"), | |
Person(4, "Ana", "5544332266") | |
) | |
} | |
fun people(): List<Person> = people | |
fun findPersonById(id: Long): Person? = people.find { it.id == id } | |
fun createPerson(person: Person): Person = person.apply { | |
id = (people.size + 1).toLong() | |
people.add(this) | |
} | |
fun updatePerson(id: Long, person: Person): Person? { | |
val personFound = findPersonById(id) | |
personFound?.let { | |
people.remove(it) | |
if (it.phone != person.phone){ | |
it.phone = person.phone | |
} | |
if (it.name != person.name){ | |
it.name = person.name | |
} | |
people.add(it) | |
} | |
return personFound | |
} | |
fun deletePersonById(id: Long): Boolean{ | |
val person = findPersonById(id) | |
person?.let { | |
return people.remove(it) | |
} | |
return false | |
} | |
} |
Para este caso tenemos que la fuente de nuestros datos son datos hardcode que en posteriores tutos haremos conexiones a SQL y NoSQL.
Creando el @Service
Un @Service tiene toda la lógica de negocio, puede tener uno ó más @Repository, intercambiará y procesará datos con el/los @Repository
interface PersonService { | |
fun getAllPeople(): List<Person> | |
fun getPersonById(id: Long): Person | |
fun createPerson(person: Person) | |
fun updatePerson(id: Long, person: Person): Person | |
fun deletePersonById(id: Long) | |
} |
Generalmente se hace un contrato (un Interface) que en la implementación se la agrega la lógica, por si en algún caso otro controller necesita el mismo service con diferente lógica ú otros @Repository, la clase que implementa la interfaz del service generalmente acaba con impl.
Ahí inyecta al @Repository y lo utiliza para intercambiar datos
@Service | |
class PersonServiceImpl: PersonService { | |
@Autowired | |
private lateinit var personRepository: PersonRepository | |
override fun getAllPeople(): List<Person> { | |
return personRepository.people() | |
} | |
override fun getPersonById(id: Long): Person { | |
return personRepository.findPersonById(id) ?: TODO("throw not found exception") | |
} | |
override fun createPerson(person: Person) { | |
personRepository.createPerson(person) | |
} | |
override fun updatePerson(id: Long, person: Person): Person { | |
return personRepository.updatePerson(id, person) ?: TODO("throw not found exception") | |
} | |
override fun deletePersonById(id: Long) { | |
personRepository.deletePersonById(id) | |
} | |
} |
En este caso no hay mucha lógica de negocio por lo que casi se llama directo, lo único que está sin implementar y por eso tiene los TODO son los notFound que deberíamos regresar un 404 pero eso lo veremos en otro tutorial para ver el manejo de errores en sprint.
Creando el @RestController
El @RestController define el uso del protocolo HTTP y de la url para el consumo del servicio, por ejemplo podemos definir que para personas el path será /person que el método POST llamara a la creación de una persona como lo vemos a continuación:
@RestController | |
@RequestMapping("person") | |
class PersonController { | |
@Autowired | |
private lateinit var personService: PersonService | |
@GetMapping | |
fun getPeople(): List<Person> = | |
personService.getAllPeople() | |
@GetMapping("/{id}") | |
fun getPersonById(@PathVariable("id") id: Long) = | |
personService.getPersonById(id) | |
@PostMapping | |
fun createPerson(@RequestBody person: Person) = | |
personService.createPerson(person) | |
@PatchMapping("/{id}") | |
fun updatePerson(@PathVariable("id") id: Long, @RequestBody person: Person) = | |
personService.updatePerson(id, person) | |
@DeleteMapping("/{id}") | |
fun deletePerson(@PathVariable("id") id: Long) = | |
personService.deletePersonById(id) | |
} |
Explico para que tanta anotación:
- @RestController: Configuramos esta clase como receptora de peticiones HTTP de los clientes.
- @RequestMapping: En este caso decimos que el path para el CRUD de persona será /person
- @Autowired: Lo usamos para inyectar nuestro @Service
- @GetMapping: Especificamos que el método GET del path raíz devolverá todas las personas es decir: GET /person
- @GetMapping(“/{id}”): El método anotado con esto va a responder a la petición GET del path /person/<id> y devolverá la persona que le corresponde el id
- @PostMapping: El método anotado con esto, responderá a la petición HTTP, método POST y con el path /person para crear una persona en la base de datos.
- @PatchMapping(“/{id}”): Indica que el método anotado responderá a la petición HTTP, método Patch y path /person/<id> para actualizar el nombre ó el teléfono del recurso correspondiente al id ingresado en el path.
- @DeleteMapping(“/{id}”): El método anotado con esto responderá a la petición HTTP, método Delete y el path /person/<id>, borrando la persona de nuestra lista en memoria.
- @PathVariable(“id”): Debe de hacer match con lo definido en los mappings lo que va en {id}, el parámetro anotado con este será el que tendrá el valor ingresado en el path de la url.
- @RequestBody: El parámetro anotado con esto será lo que se debe enviar en el body de los métodos: POST, PUT ó PATCH
Probemos el resultado!
Pongamos a correr nuestro programa! y hagamos lo que indica en la imagen para esto yo uso postman como cliente.
En el siguiente post veremos el manejo de errores al final es tener una API completa como se usa en la industria!
MUCHAS GRACIAS!