Les lecteurs de mes publications connaissent probablement l’idée d’utiliser une approche API First pour développer des microservices. D’innombrables fois, j’ai réalisé les avantages de décrire les URI anticipés et les modèles d’objet sous-jacents avant le début de tout développement.
Cependant, au cours de mes 30 années de navigation dans la technologie, j'en suis venu à m'attendre aux réalités de flux alternatifs . En d’autres termes, je m’attends à ce qu’il y ait des situations dans lesquelles API First n’est tout simplement pas possible.
Pour cet article, je voulais présenter un exemple de la manière dont les équipes produisant des microservices peuvent toujours réussir à fournir une spécification OpenAPI que d'autres peuvent utiliser sans définir manuellement un fichier openapi.json.
Je voulais également sortir de ma zone de confort et le faire sans utiliser Java, .NET ou même JavaScript.
À la fin de la plupart de mes articles, je mentionne souvent ma déclaration de mission personnelle :
« Concentrez votre temps sur la fourniture de caractéristiques/fonctionnalités qui augmentent la valeur de votre propriété intellectuelle. Tirez parti des frameworks, des produits et des services pour tout le reste. – J.Vester
Mon objectif dans cet énoncé de mission est de me responsabiliser pour utiliser au mieux mon temps lorsque j'essaie d'atteindre des buts et des objectifs fixés à un niveau supérieur. Fondamentalement, si notre objectif est de vendre plus de widgets, je devrais consacrer mon temps à trouver des moyens de rendre cela possible, en évitant les défis qui ont déjà été résolus par les cadres, produits ou services existants.
J'ai choisi Python comme langage de programmation pour mon nouveau microservice. À ce jour, 99 % du code Python que j'ai écrit pour mes articles précédents est le résultat de réponses basées sur Stack Overflow Driven Development (SODD) ou ChatGPT. De toute évidence, Python sort de ma zone de confort.
Maintenant que j'ai défini la situation, je voulais créer un nouveau microservice RESTful basé sur Python qui adhère à mon énoncé de mission personnel avec une expérience minimale dans la langue source.
C'est alors que j'ai trouvé FastAPI .
FastAPI existe depuis 2018 et est un framework axé sur la fourniture d'API RESTful à l'aide d'indices de type Python. La meilleure partie de FastAPI est la possibilité de générer automatiquement des spécifications OpenAPI 3 sans aucun effort supplémentaire du point de vue du développeur.
Pour cet article, l'idée d'une API d'article m'est venue à l'esprit, fournissant une API RESTful qui permet aux consommateurs de récupérer une liste de mes articles récemment publiés.
Pour simplifier les choses, supposons qu'un Article
donné contienne les propriétés suivantes :
id
– propriété d’identifiant simple et unique (numéro)title
– le titre de l'article (chaîne)url
– l'URL complète de l'article (chaîne)year
– l’année où l’article a été publié (numéro)
L'API Article comprendra les URI suivants :
/articles
– récupérera une liste d’articles/articles/{article_id}
– récupérera un seul article par la propriété id/articles
– ajoute un nouvel articleDans mon terminal, j'ai créé un nouveau projet Python appelé fast-api-demo puis exécuté les commandes suivantes :
$ pip install --upgrade pip $ pip install fastapi $ pip install uvicorn
J'ai créé un nouveau fichier Python appelé api.py
et ajouté quelques importations, ainsi que créé une 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)
Ensuite, j'ai défini un objet Article
pour correspondre au cas d'utilisation de l'API Article :
class Article(BaseModel): id: int title: str url: str year: int
Une fois le modèle établi, j'ai dû ajouter les URI… ce qui s'est avéré assez simple :
# 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")
Pour m'éviter d'impliquer un magasin de données externe, j'ai décidé d'ajouter par programme certains de mes articles récemment publiés :
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), ]
Croyez-le ou non, cela termine le développement du microservice Article API.
Pour une vérification rapide de l'intégrité, j'ai lancé mon service API localement :
$ 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)
Ensuite, dans une autre fenêtre de terminal, j'ai envoyé une requête curl (et je l'ai transmise à 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 }
Plutôt que de simplement exécuter l'API Article localement, j'ai pensé voir avec quelle facilité je pourrais déployer le microservice. Comme je n'avais jamais déployé de microservice Python sur Heroku auparavant, j'avais l'impression que ce serait le moment idéal pour essayer.
Avant de plonger dans Heroku, je devais créer un fichier requirements.txt
pour décrire les dépendances du service. Pour ce faire, j'ai installé et exécuté pipreqs
:
$ pip install pipreqs $ pipreqs
Cela a créé un fichier requirements.txt
pour moi, avec les informations suivantes :
fastapi==0.110.1 pydantic==2.6.4 uvicorn==0.29.0
J'avais également besoin d'un fichier appelé Procfile
qui indique à Heroku comment démarrer mon microservice avec uvicorn
. Son contenu ressemblait à ceci :
web: uvicorn api:app --host=0.0.0.0 --port=${PORT}
Pour ceux d'entre vous qui sont nouveaux sur Python (comme moi), j'ai utilisé la documentation Mise en route sur Heroku avec Python comme guide utile.
Comme j'avais déjà installé la CLI Heroku, il me suffisait de me connecter à l'écosystème Heroku depuis mon terminal :
$ heroku login
Je me suis assuré d'enregistrer toutes mes mises à jour dans mon référentiel sur GitLab.
Ensuite, la création d'une nouvelle application dans Heroku peut être réalisée à l'aide de la CLI via la commande suivante :
$ heroku create
La CLI a répondu avec un nom d'application unique, ainsi que l'URL de l'application et le référentiel git associé à l'application :
Creating app... done, powerful-bayou-23686 https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/ | https://git.heroku.com/powerful-bayou-23686.git
Veuillez noter qu'au moment où vous lirez cet article, mon application ne sera plus en ligne.
Regarde ça. Lorsque j'émets une commande git distante, je peux voir qu'une télécommande a été automatiquement ajoutée à l'écosystème Heroku :
$ git remote heroku origin
Pour déployer l'application fast-api-demo
sur Heroku, il me suffit d'utiliser la commande suivante :
$ git push heroku main
Une fois tout configuré, j'ai pu valider que mon nouveau service basé sur Python est opérationnel dans le tableau de bord Heroku :
Avec le service en cours d'exécution, il est possible de récupérer l' Article
avec id = 1
depuis l'API Article en exécutant la commande curl suivante :
$ curl --location 'https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/articles/1'
La commande curl renvoie une réponse 200 OK et la charge utile JSON suivante :
{ "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 }
L'exploitation de la fonctionnalité OpenAPI intégrée de FastAPI permet aux consommateurs de recevoir une spécification v3 entièrement fonctionnelle en accédant à l'URI /docs
généré automatiquement :
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/docs
L'appel de cette URL renvoie le microservice de l'API Article à l'aide de l'interface utilisateur Swagger largement adoptée :
Pour ceux qui recherchent un fichier openapi.json
pour générer des clients pour consommer l'API Article, l'URI /openapi.json
peut être utilisé :
https://powerful-bayou-23686-2d5be7cf118b.herokuapp.com/openapi.json
Pour mon exemple, la spécification OpenAPI v3 basée sur JSON apparaît comme indiqué ci-dessous :
{ "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" } } } }
Par conséquent, la spécification suivante peut être utilisée pour générer des clients dans un certain nombre de langues différentes via OpenAPI Generator .
Au début de cet article, j’étais prêt à aller au combat et à affronter toute personne non intéressée par l’utilisation d’une approche API First. Ce que j'ai appris de cet exercice, c'est qu'un produit comme FastAPI peut aider à définir et à produire rapidement un microservice RESTful fonctionnel tout en incluant également une spécification OpenAPI v3 entièrement consommable… automatiquement.
Il s'avère que FastAPI permet aux équipes de rester concentrées sur leurs buts et objectifs en tirant parti d'un cadre qui génère un contrat standardisé sur lequel les autres peuvent s'appuyer. En conséquence, une autre voie a émergé pour adhérer à ma déclaration de mission personnelle.
En cours de route, j'ai utilisé Heroku pour la première fois pour déployer un service basé sur Python. Cela s’est avéré nécessiter peu d’efforts de ma part, mis à part la révision d’une documentation bien rédigée. Un autre bonus de mission doit donc également être mentionné pour la plate-forme Heroku.
Si vous êtes intéressé par le code source de cet article, vous pouvez le trouver sur GitLab .
Passez une très bonne journée !