Los lectores de mis publicaciones probablemente estén familiarizados con la idea de emplear un enfoque API First para desarrollar microservicios. En innumerables ocasiones, me he dado cuenta de los beneficios de describir los URI anticipados y los modelos de objetos subyacentes antes de que comience cualquier desarrollo.
Sin embargo, en mis más de 30 años de navegación en tecnología, he llegado a esperar las realidades de flujos alternativos . En otras palabras, espero que haya situaciones en las que API First simplemente no sea posible.
Para este artículo, quería mostrar un ejemplo de cómo los equipos que producen microservicios aún pueden tener éxito al proporcionar una especificación OpenAPI para que otros la consuman sin definir manualmente un archivo openapi.json.
También quería salir de mi zona de confort y hacer esto sin usar Java, .NET o incluso JavaScript.
Al final de la mayoría de mis artículos, a menudo menciono mi declaración de misión personal:
“Concentre su tiempo en ofrecer características/funcionalidades que amplíen el valor de su propiedad intelectual. Aproveche los marcos, productos y servicios para todo lo demás”. – J. Vester
Mi punto en esta declaración de misión es hacerme responsable de hacer el mejor uso de mi tiempo cuando intento alcanzar metas y objetivos establecidos en un nivel superior. Básicamente, si nuestro objetivo es vender más widgets, debería dedicar mi tiempo a encontrar formas de hacerlo posible, evitando los desafíos que ya han sido resueltos por los marcos, productos o servicios existentes.
Elegí Python como lenguaje de programación para mi nuevo microservicio. Hasta la fecha, el 99% del código Python que escribí para mis artículos anteriores ha sido el resultado de respuestas basadas en Stack Overflow Driven Development (SODD) o ChatGPT. Claramente, Python queda fuera de mi zona de confort.
Ahora que he definido dónde están las cosas, quería crear un nuevo microservicio RESTful basado en Python que se adhiera a mi declaración de misión personal con una experiencia mínima en el lenguaje fuente.
Fue entonces cuando encontré FastAPI .
FastAPI existe desde 2018 y es un marco centrado en ofrecer API RESTful utilizando sugerencias de tipo Python. La mejor parte de FastAPI es la capacidad de generar automáticamente especificaciones OpenAPI 3 sin ningún esfuerzo adicional desde la perspectiva del desarrollador.
Para este artículo, me vino a la mente la idea de una API de artículos, que proporcione una API RESTful que permita a los consumidores recuperar una lista de mis artículos publicados recientemente.
Para simplificar las cosas, supongamos que un Article
determinado contiene las siguientes propiedades:
id
: propiedad de identificador simple y única (número)title
: el título del artículo (cadena)url
: la URL completa del artículo (cadena)year
– el año en que se publicó el artículo (número)
La API del artículo incluirá los siguientes URI:
/articles
: recuperará una lista de artículos/articles/{article_id}
– recuperará un único artículo mediante la propiedad id/articles
– agrega un nuevo artículoEn mi terminal, creé un nuevo proyecto de Python llamado fast-api-demo y luego ejecuté los siguientes comandos:
$ pip install --upgrade pip $ pip install fastapi $ pip install uvicorn
Creé un nuevo archivo Python llamado api.py
y agregué algunas importaciones, además establecí una variable app
:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() if __name__ == "__main__": import uvicorn uvicorn.run(app, host="localhost", port=8000)
A continuación, definí un objeto Article
para que coincida con el caso de uso de la API de artículo:
class Article(BaseModel): id: int title: str url: str year: int
Con el modelo establecido, necesitaba agregar las URI… lo cual resultó ser bastante fácil:
# Route to add a new article @app.post("/articles") def create_article(article: Article): articles.append(article) return article # Route to get all articles @app.get("/articles") def get_articles(): return articles # Route to get a specific article by ID @app.get("/articles/{article_id}") def get_article(article_id: int): for article in articles: if article.id == article_id: return article raise HTTPException(status_code=404, detail="Article not found")
Para evitar involucrarme en un almacén de datos externo, decidí agregar algunos de mis artículos publicados recientemente mediante programación:
articles = [ Article(id=1, title="Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", url="https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", year=2023), Article(id=2, title="Using Unblocked to Fix a Service That Nobody Owns", url="https://dzone.com/articles/using-unblocked-to-fix-a-service-that-nobody-owns", year=2023), Article(id=3, title="Exploring the Horizon of Microservices With KubeMQ's New Control Center", url="https://dzone.com/articles/exploring-the-horizon-of-microservices-with-kubemq", year=2024), Article(id=4, title="Build a Digital Collectibles Portal Using Flow and Cadence (Part 1)", url="https://dzone.com/articles/build-a-digital-collectibles-portal-using-flow-and-1", year=2024), Article(id=5, title="Build a Flow Collectibles Portal Using Cadence (Part 2)", url="https://dzone.com/articles/build-a-flow-collectibles-portal-using-cadence-par-1", year=2024), Article(id=6, title="Eliminate Human-Based Actions With Automated Deployments: Improving Commit-to-Deploy Ratios Along the Way", url="https://dzone.com/articles/eliminate-human-based-actions-with-automated-deplo", year=2024), Article(id=7, title="Vector Tutorial: Conducting Similarity Search in Enterprise Data", url="https://dzone.com/articles/using-pgvector-to-locate-similarities-in-enterpris", year=2024), Article(id=8, title="DevSecOps: It's Time To Pay for Your Demand, Not Ingestion", url="https://dzone.com/articles/devsecops-its-time-to-pay-for-your-demand", year=2024), ]
Lo creas o no, esto completa el desarrollo del microservicio Article API.
Para una comprobación rápida de la cordura, activé mi servicio API localmente:
$ python api.py INFO: Started server process [320774] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
Luego, en otra ventana de terminal, envié una solicitud curl (y la envié a json_pp
):
$ curl localhost:8000/articles/1 | json_pp { "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
En lugar de simplemente ejecutar la API del artículo localmente, pensé en ver con qué facilidad podía implementar el microservicio. Como nunca antes había implementado un microservicio de Python en Heroku , sentí que ahora sería un buen momento para intentarlo.
Antes de sumergirme en Heroku, necesitaba crear un archivo requirements.txt
para describir las dependencias del servicio. Para hacer esto, instalé y ejecuté pipreqs
:
$ pip install pipreqs $ pipreqs
Esto creó un archivo requirements.txt
para mí, con la siguiente información:
fastapi==0.110.1 pydantic==2.6.4 uvicorn==0.29.0
También necesitaba un archivo llamado Procfile
que le indica a Heroku cómo activar mi microservicio con uvicorn
. Su contenido se veía así:
web: uvicorn api:app --host=0.0.0.0 --port=${PORT}
Para aquellos de ustedes que son nuevos en Python (como yo), utilicé la documentación Introducción a Heroku con Python como guía útil.
Como ya tenía instalada la CLI de Heroku, solo necesitaba iniciar sesión en el ecosistema de Heroku desde mi terminal:
$ heroku login
Me aseguré de registrar todas mis actualizaciones en mi repositorio en GitLab.
A continuación, la creación de una nueva aplicación en Heroku se puede realizar utilizando la CLI mediante el siguiente comando:
$ heroku create
La CLI respondió con un nombre de aplicación único, junto con la URL de la aplicación y el repositorio basado en Git asociado con la aplicación:
Creating app... done, powerful-bayou-23686 https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/ | https://git.heroku.com/powerful-bayou-23686.git
Tenga en cuenta que cuando lea este artículo, mi aplicación ya no estará en línea.
Mira esto. Cuando ejecuto un comando remoto de git, puedo ver que se agregó automáticamente un control remoto al ecosistema Heroku:
$ git remote heroku origin
Para implementar la aplicación fast-api-demo
en Heroku, todo lo que tengo que hacer es usar el siguiente comando:
$ git push heroku main
Con todo configurado, pude validar que mi nuevo servicio basado en Python está funcionando en el panel de Heroku:
Con el servicio en ejecución, es posible recuperar el Article
con id = 1
de la API del artículo emitiendo el siguiente comando curl:
$ curl --location 'https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/articles/1'
El comando curl devuelve una respuesta 200 OK y la siguiente carga JSON:
{ "id": 1, "title": "Distributed Cloud Architecture for Resilient Systems: Rethink Your Approach To Resilient Cloud Services", "url": "https://dzone.com/articles/distributed-cloud-architecture-for-resilient-syste", "year": 2023 }
Aprovechar la funcionalidad OpenAPI integrada de FastAPI permite a los consumidores recibir una especificación v3 completamente funcional navegando al URI /docs
generado automáticamente:
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/docs
Llamar a esta URL devuelve el microservicio Article API utilizando la interfaz de usuario Swagger ampliamente adoptada:
Para aquellos que buscan un archivo openapi.json
para generar clientes que consuman la API del artículo, se puede utilizar el URI /openapi.json
:
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/openapi.json
Para mi ejemplo, la especificación OpenAPI v3 basada en JSON aparece como se muestra a continuación:
{ "openapi": "3.1.0", "info": { "title": "FastAPI", "version": "0.1.0" }, "paths": { "/articles": { "get": { "summary": "Get Articles", "operationId": "get_articles_articles_get", "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } } } }, "post": { "summary": "Create Article", "operationId": "create_article_articles_post", "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Article" } } }, "required": true }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } }, "/articles/{article_id}": { "get": { "summary": "Get Article", "operationId": "get_article_articles__article_id__get", "parameters": [ { "name": "article_id", "in": "path", "required": true, "schema": { "type": "integer", "title": "Article Id" } } ], "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { } } } }, "422": { "description": "Validation Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HTTPValidationError" } } } } } } } }, "components": { "schemas": { "Article": { "properties": { "id": { "type": "integer", "title": "Id" }, "title": { "type": "string", "title": "Title" }, "url": { "type": "string", "title": "Url" }, "year": { "type": "integer", "title": "Year" } }, "type": "object", "required": [ "id", "title", "url", "year" ], "title": "Article" }, "HTTPValidationError": { "properties": { "detail": { "items": { "$ref": "#/components/schemas/ValidationError" }, "type": "array", "title": "Detail" } }, "type": "object", "title": "HTTPValidationError" }, "ValidationError": { "properties": { "loc": { "items": { "anyOf": [ { "type": "string" }, { "type": "integer" } ] }, "type": "array", "title": "Location" }, "msg": { "type": "string", "title": "Message" }, "type": { "type": "string", "title": "Error Type" } }, "type": "object", "required": [ "loc", "msg", "type" ], "title": "ValidationError" } } } }
Como resultado, la siguiente especificación se puede utilizar para generar clientes en varios idiomas diferentes a través de OpenAPI Generator .
Al comienzo de este artículo, estaba listo para ir a la batalla y enfrentarme a cualquiera que no estuviera interesado en utilizar un enfoque API First. Lo que aprendí de este ejercicio es que un producto como FastAPI puede ayudar a definir y producir un microservicio RESTful funcional rápidamente y al mismo tiempo incluir una especificación OpenAPI v3 totalmente consumible... automáticamente.
Resulta que FastAPI permite a los equipos mantenerse enfocados en sus metas y objetivos al aprovechar un marco que genera un contrato estandarizado en el que otros pueden confiar. Como resultado, ha surgido otro camino para adherirme a mi declaración de misión personal.
En el camino, utilicé Heroku por primera vez para implementar un servicio basado en Python. Resultó que esto requirió poco esfuerzo de mi parte, aparte de revisar alguna documentación bien escrita. Por lo tanto, también es necesario mencionar otra bonificación de la declaración de misión para la plataforma Heroku.
Si está interesado en el código fuente de este artículo, puede encontrarlo en GitLab .
¡Que tengas un gran día!