Fabriquer des API avec des rails

De nos jours, il est courant de faire largement appel à des API (interfaces de programmation d'applications). Non seulement les grands services comme Facebook et Twitter les utilisent: les API sont très populaires en raison de la multiplication des infrastructures côté client telles que React, Angular et bien d'autres. Ruby on Rails suit cette tendance et la dernière version présente une nouvelle fonctionnalité vous permettant de créer des applications uniquement API.. 

Initialement, cette fonctionnalité était intégrée à une gemme séparée appelée rails-api, mais depuis la publication de Rails 5, elle fait désormais partie du noyau du framework. Cette fonctionnalité, avec ActionCable, était probablement la plus attendue. Nous allons donc en discuter aujourd’hui..

Cet article explique comment créer des applications Rails uniquement basées sur des API et explique comment structurer vos routes et vos contrôleurs, répondre au format JSON, ajouter des sérialiseurs et configurer le partage de ressources CORS (Cross-Origin Resource Sharing). Vous apprendrez également certaines options pour sécuriser l'API et la protéger contre les abus..

La source de cet article est disponible sur GitHub.

Création d'une application uniquement API

Pour commencer, lancez la commande suivante:

rails nouveaux RailsApiDemo --api

Il va créer une nouvelle application Rails réservée à l’API, appelée RailsApiDemo. N'oubliez pas que le soutien à la --api Cette option a été ajoutée uniquement dans Rails 5, assurez-vous donc que cette version ou une version plus récente est installée..

Ouvrez le Gemfile et notez qu'il est beaucoup plus petit que d'habitude: des gemmes comme bar à café, turbolinks, et sass-rails sont partis.

le config / application.rb Le fichier contient une nouvelle ligne:

config.api_only = true

Cela signifie que Rails va charger un plus petit ensemble de middleware: par exemple, il n'y a pas de cookies et de support de sessions. De plus, si vous essayez de générer un échafaudage, les vues et les actifs ne seront pas créés. En fait, si vous cochez la case vues / mises en page répertoire, vous remarquerez que le application.html.erb le fichier est manquant aussi.

Une autre différence importante est que le ApplicationController hérite de la ActionController :: API, ne pas ActionController :: Base.

En gros, il s’agit là d’une application Rails de base que vous avez vue à plusieurs reprises. Ajoutons maintenant quelques modèles pour que nous puissions travailler avec:

rails g model Nom d'utilisateur: string rails g model Titre du message: string body: text utilisateur: appartient_to rails db: migrate

Rien de spécial ne se passe ici: un article avec un titre et un corps appartenant à un utilisateur.

Assurez-vous que les associations appropriées sont configurées et fournissez également quelques contrôles de validation simples:

models / user.rb

 has_many: posts valide: nom, présence: true

modèles / post.rb

 appartient_à: l'utilisateur valide: titre, présence: vrai valide: corps, présence: vrai

Brillant! L'étape suivante consiste à charger quelques exemples d'enregistrements dans les tables nouvellement créées..

Chargement des données de démonstration

Le moyen le plus simple de charger certaines données consiste à utiliser le seeds.rb déposer à l'intérieur du db annuaire. Cependant, je suis paresseux (comme le sont beaucoup de programmeurs) et ne veux pas penser à un exemple de contenu. Par conséquent, pourquoi ne profitons-nous pas de la fausse pierre précieuse qui peut produire des données aléatoires de différentes sortes: noms, courriels, mots branchés, textes de "lorem ipsum", et bien plus encore.

Gemfile

groupe: développement do gem 'faker' end

Installez la gemme:

installation groupée

Maintenant, modifiez le seeds.rb:

db / seeds.rb

5. Parfois, user = User.create (name: Faker :: Name.name) user.posts.create (title: Faker :: Book.title, body: Faker :: Lorem.sentence) end

Enfin, chargez vos données:

rails db: seed

Répondre avec JSON

Maintenant, bien sûr, nous avons besoin de routes et de contrôleurs pour concevoir notre API. Il est courant d’implémenter les routes de l’API sous le api / chemin. En outre, les développeurs fournissent généralement la version de l'API dans le chemin, par exemple api / v1 /. Plus tard, si des changements radicaux doivent être introduits, vous pouvez simplement créer un nouvel espace de nom (v2) et un contrôleur séparé.

Voici à quoi peuvent ressembler vos itinéraires:

config / routes.rb

namespace 'api' do namespace 'v1' do resources: publie des ressources: les utilisateurs end end

Cela génère des itinéraires comme:

api_v1_posts GET /api/v1/posts(.:format) api / v1 / posts # index POST /api/v1/posts(.:format) api / v1 / posts # créer api_v1_post GET / api / v1 / posts /: id (.: format) api / v1 / posts # show

Vous pouvez utiliser un portée méthode au lieu de espace de noms, mais par défaut, il cherchera le UtilisateursContrôleur et PostsController à l'intérieur de contrôleurs répertoire, pas à l'intérieur du contrôleurs / api / v1, donc sois prudent.

Créer le api dossier avec le répertoire imbriqué v1 à l'intérieur de contrôleurs. Remplissez-le avec vos contrôleurs:

controllers / api / v1 / users_controller.rb

module Api module classe V1 UsersController < ApplicationController end end end

controllers / api / v1 / posts_controller.rb

module Api module V1 class PostsController < ApplicationController end end end

Notez que vous devez non seulement imbriquer le fichier du contrôleur sous le api / v1 chemin, mais la classe elle-même doit également être namespaced à l'intérieur du Api et V1 modules.

La prochaine question est de savoir comment répondre correctement avec les données au format JSON. Dans cet article, nous allons essayer ces solutions: les gems jBuilder et active_model_serializers. Donc, avant de passer à la section suivante, déposez-les dans le Gemfile:

Gemfile

gem 'jbuilder', '~> 2.5' gem 'active_model_serializers', '~> 0.10.0'

Puis lancez:

installation groupée

Utilisation de la gemme jBuilder

jBuilder est un joyau populaire géré par l'équipe Rails qui fournit un simple DSL (langage spécifique au domaine) vous permettant de définir des structures JSON dans vos vues..

Supposons que nous voulions afficher tous les articles lorsqu'un utilisateur clique sur le indice action:

controllers / api / v1 / posts_controller.rb

 def index @posts = Post.order ('created_at DESC') end

Tout ce que vous avez à faire est de créer la vue nommée d'après l'action correspondante avec le .json.jbuilder extension. Notez que la vue doit être placée sous le api / v1 chemin aussi:

views / api / v1 / posts / index.json.jbuilder

json.array! @posts do | post | json.id post.id json.title post.title json.body post.body end

json.array! traverse le @des postes tableau. json.id, json.title et json.body générer les clés avec les noms correspondants en définissant les arguments comme valeurs. Si vous accédez à http: // localhost: 3000 / api / v1 / posts.json, vous verrez une sortie similaire à celle-ci:

["id": 1, "titre": "Titre 1", "corps": "Corps 1", "id": 2, "titre": "Titre 2", "corps": "Corps 2 "]

Et si nous voulions aussi afficher l'auteur pour chaque message? C'est simple:

json.array! @posts do | post | json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name fin fin

La sortie deviendra:

["id": 1, "titre": "Titre 1", "corps": "Corps 1", "utilisateur": "id": 1, "nom": "nom d'utilisateur"]

Le contenu de la .constructeur fichiers est du code Ruby simple, vous pouvez donc utiliser toutes les opérations de base comme d'habitude.

Notez que jBuilder prend en charge les partiels comme n'importe quelle vue Rails ordinaire. Vous pouvez donc également indiquer: 

json.partial! partiel: 'posts / post', collection: @posts, as:: post

puis créer le views / api / v1 / posts / _post.json.jbuilder fichier avec le contenu suivant:

json.id post.id json.title post.title json.body post.body json.user do json.id post.user.id json.name post.user.name end

Donc, comme vous le voyez, jBuilder est simple et pratique. Cependant, au lieu de cela, vous pouvez vous en tenir aux sérialiseurs. Discutons-en dans la section suivante..

Utilisation de sérialiseurs

La gem rails_model_serializers a été créé par une équipe qui gérait initialement le rail-api. Comme indiqué dans la documentation, rails_model_serializers apporte la convention sur la configuration à votre génération JSON. En gros, vous définissez quels champs doivent être utilisés lors de la sérialisation (c'est-à-dire, la génération JSON).

Voici notre premier sérialiseur:

sérialiseurs / post_serializer.rb

classe PostSerializer < ActiveModel::Serializer attributes :id, :title, :body end

Nous disons ici que tous ces champs doivent être présents dans le JSON résultant. Maintenant des méthodes comme to_json et as_json appelé un message utilisera cette configuration et renverra le contenu approprié.

Pour le voir en action, modifiez le indice action comme celle-ci:

controllers / api / v1 / posts_controller.rb

def index @posts = Post.order ('created_at DESC') render json: @posts end

as_json sera automatiquement appelé à la @des postes objet.

Qu'en est-il des utilisateurs? Les sérialiseurs vous permettent d'indiquer des relations, tout comme les modèles. De plus, les sérialiseurs peuvent être imbriqués:

sérialiseurs / post_serializer.rb

classe PostSerializer < ActiveModel::Serializer attributes :id, :title, :body belongs_to :user class UserSerializer < ActiveModel::Serializer attributes :id, :name end end

Maintenant, lorsque vous sérialisez la publication, elle contiendra automatiquement la utilisateur clé avec son identifiant et son nom. Si plus tard vous créez un sérialiseur séparé pour l’utilisateur avec le : id attribut exclu:

sérialiseurs / post_serializer.rb

classe UserSerializer < ActiveModel::Serializer attributes :name end

puis @ user.as_json ne retournera pas l'identifiant de l'utilisateur. Encore, @ post.as_json renverra à la fois le nom et l'identifiant de l'utilisateur, alors gardez-le à l'esprit.

Sécuriser l'API

Dans de nombreux cas, nous ne voulons pas que quiconque exécute une action en utilisant l'API. Présentons donc un simple contrôle de sécurité et obligeons nos utilisateurs à envoyer leurs jetons lors de la création et de la suppression de messages..

Le jeton aura une durée de vie illimitée et sera créé lors de l'enregistrement de l'utilisateur. Tout d’abord, ajoutez un nouveau jeton colonne à la utilisateurs table:

rails g migration jeton add_token_to_users: chaîne: index

Cet index devrait garantir l'unicité car il ne peut y avoir deux utilisateurs avec le même jeton:

db / migrate / xyz_add_token_to_users.rb

add_index: utilisateurs,: jeton, unique: true

Appliquer la migration:

rails db: migrer

Maintenant, ajoutez le before_save rappeler:

models / user.rb

before_create -> self.token = generate_token

le generate_token méthode privée va créer un jeton dans un cycle sans fin et vérifier si c'est unique ou pas. Dès qu'un jeton unique est trouvé, retournez-le:

models / user.rb

private def generate_token loop doken = SecureRandom.hex renvoie un jeton sauf si User.exists? (token: token) end end

Vous pouvez utiliser un autre algorithme pour générer le jeton, par exemple en vous basant sur le hachage MD5 du nom de l'utilisateur et du sel..

Enregistrement de l'utilisateur

Bien sûr, nous devons également permettre aux utilisateurs de s’inscrire, sinon ils ne pourront pas obtenir leur jeton. Je ne souhaite pas introduire de vues HTML dans notre application, ajoutons donc une nouvelle méthode API:

controllers / api / v1 / users_controller.rb

def create @user = User.new (user_params) if @ user.save état du rendu:: created else rendu json: @ user.errors, statut:: UNNENTABLE_ENTENT_FR End end private def user_params params.require (: user) .permit (: nom) fin

Il est judicieux de renvoyer des codes d'état HTTP significatifs afin que les développeurs comprennent exactement ce qui se passe. Maintenant, vous pouvez soit fournir un nouveau sérialiseur aux utilisateurs, soit vous en tenir à une .json.jbuilder fichier. Je préfère cette dernière variante (c’est pourquoi je ne passe pas la : json option au rendre méthode), mais vous êtes libre de choisir l’un d’eux. Notez cependant que le jeton ne doit pas être toujours sérialisé, par exemple lorsque vous renvoyez une liste de tous les utilisateurs, il convient de la garder en sécurité!

views / api / v1 / utilisateurs / create.json.jbuilder

json.id @ user.id json.name @ user.name json.token @ user.token

La prochaine étape consiste à vérifier si tout fonctionne correctement. Vous pouvez soit utiliser le boucle commande ou écrit du code Ruby. Puisque cet article concerne Ruby, je vais aller avec l'option de codage.

Test de l'enregistrement de l'utilisateur

Pour effectuer une requête HTTP, nous allons utiliser la gem Faraday, qui fournit une interface commune sur plusieurs adaptateurs (la valeur par défaut est Net :: HTTP). Créez un fichier Ruby distinct, incluez Faraday et configurez le client:

api_client.rb

Requiert le client 'faraday' = Faraday.new (url: 'http: // localhost: 3000') do | config | config.adapter Faraday.default_adapter end response = client.post do | req | req.url '/ api / v1 / user' req.headers ['Content-Type'] = 'application / json' req.body = '"utilisateur": "nom": "utilisateur test" "fin

Toutes ces options sont assez explicites: nous choisissons l’adaptateur par défaut, définissons l’URL de la demande sur http: // localhost: 300 / api / v1 / users, modifions le type de contenu en application / json, et fournir le corps de notre demande.

La réponse du serveur va contenir JSON, donc pour l'analyse, j'utiliserai la gemme Oj:

api_client.rb

require 'oj' # client here… met Oj.load (response.body) met response.status

Outre la réponse analysée, j'affiche également le code d'état à des fins de débogage.

Maintenant, vous pouvez simplement exécuter ce script:

ruby api_client.rb

et stocker le jeton reçu quelque part, nous l'utilisons dans la section suivante.

Authentification avec le jeton

Pour appliquer l’authentification par jeton, le authenticate_or_request_with_http_token méthode peut être utilisé. Il fait partie du module ActionController :: HttpAuthentication :: Token :: ControllerMethods, alors n'oubliez pas de l'inclure:

controllers / api / v1 / posts_controller.rb 

classe PostsController < ApplicationController include ActionController::HttpAuthentication::Token::ControllerMethods #… end

Ajouter un nouveau before_action et la méthode correspondante:

controllers / api / v1 / posts_controller.rb 

before_action: authentifier, uniquement: [: créer,: détruire] #… privé #… def authentifier authentifier_ou_réqueter_avec_http_token faire | jeton, options | @user = User.find_by (token: token) end end

Maintenant, si le jeton n'est pas défini ou si un utilisateur avec ce jeton est introuvable, une erreur 401 sera renvoyée, interrompant l'exécution de l'action..

Notez que la communication entre le client et le serveur doit être établie via HTTPS, sinon les jetons peuvent être facilement usurpés. Bien entendu, la solution fournie n’est pas idéale et, dans de nombreux cas, il est préférable d’utiliser le protocole OAuth 2 pour l’authentification. Il existe au moins deux joyaux qui simplifient grandement le processus de prise en charge de cette fonctionnalité: Doorkeeper et oPRO..

Créer un poste

Pour voir notre authentification en action, ajoutez le créer action à la PostsController:

controllers / api / v1 / posts_controller.rb 

def create @post = @ user.posts.new (post_params) si @ post.save rend json: @post, statut:: créé autrement rend json: @ post.errors, statut: :Unksable_entity end end

Nous profitons du sérialiseur ici pour afficher le bon JSON. le @utilisateur était déjà placé à l'intérieur du before_action.

Maintenant, testez tout avec ce code simple:

api_client.rb

client = Faraday.new (url: 'http: // localhost: 3000') do | config | config.adapter Faraday.default_adapter config.token_auth ('127a74dbec6f156401b236d6cb32db0d') end response = client.post do | req | req.url '/ api / v1 / posts' req.headers ['Content-Type'] = 'application / json' req.body = '"post": "titre": "Titre", "corps": "Texte" 'fin

Remplacer l'argument passé à la token_auth avec le jeton reçu lors de l'inscription, et lancez le script.

ruby api_client.rb

Supprimer une publication

La suppression d'un message se fait de la même manière. Ajouter le détruire action:

controllers / api / v1 / posts_controller.rb 

def destroy @post = @ user.posts.find_by (params [: id]) si @post @ post.destroy sinon restitue json: post: "non trouvé", statut:: not_found fin

Nous n'autorisons que les utilisateurs à détruire les publications qu'ils possèdent. Si la publication est supprimée avec succès, le code de statut 204 (pas de contenu) sera renvoyé. Alternativement, vous pouvez répondre avec l'identifiant de l'article qui a été supprimé car il sera toujours disponible à partir de la mémoire..

Voici le morceau de code pour tester cette nouvelle fonctionnalité:

api_client.rb

response = client.delete do | req | req.url '/ api / v1 / posts / 6' req.headers ['Content-Type'] = 'application / json' end

Remplacez l'identifiant de la publication par un numéro qui vous convient.

Configuration de la CORS

Si vous souhaitez autoriser d'autres services Web à accéder à votre API (côté client), le partage de ressources CORS (origine croisée) doit être correctement configuré. Fondamentalement, CORS permet aux applications Web d’envoyer des demandes AJAX aux services tiers. Heureusement, il existe un joyau appelé rack-cors qui nous permet de tout configurer facilement. Ajoutez-le dans le Gemfile:

Gemfile

bijou 'rack-cors'

Installez-le:

installation groupée

Et puis fournir la configuration à l'intérieur du config / initializers / cors.rb fichier. En fait, ce fichier est déjà créé pour vous et contient un exemple d'utilisation. Vous pouvez également trouver de la documentation assez détaillée sur la page de la gemme.

La configuration suivante, par exemple, permettra à quiconque d'accéder à votre API par n'importe quelle méthode:

config / initializers / cors.rb

Rails.application.config.middleware.insert_before 0, Rack :: Cors autorise les origines '*' ressource '/ api / *', en-têtes:: tous, méthodes: [: obtenir,: publier,: mettre,: patch, : supprimer,: options,: tête] fin fin

Prévenir les abus

La dernière chose que je vais mentionner dans ce guide concerne la protection de votre API contre les attaques par abus et par déni de service. Il existe un joyau appelé rack-attack (créé par des personnes de Kickstarter) qui vous permet de mettre des clients sur une liste noire ou blanche, d'empêcher le serveur d'inonder de requêtes, etc..

Déposer la gemme dans Gemfile:

Gemfile

bijou 'rack-attaque'

Installez-le:

installation groupée

Et puis fournir la configuration à l'intérieur du rack_attack.rb fichier d'initialisation. La documentation de la gemme répertorie toutes les options disponibles et suggère des cas d'utilisation. Voici l'exemple de configuration qui empêche toute personne autre que vous d'accéder au service et limite le nombre maximal de demandes à 5 par seconde:

config / initializers / rack_attack.rb

class Rack :: Attack safelist ('allow from localhost') fait | req | # Les requêtes sont autorisées si la valeur de retour est true '127.0.0.1' == req.ip || ':: 1' == req.ip end throttle ('req / ip',: limit => 5,: period => 1.second) do | req | req.ip end end

Une autre chose à faire consiste à inclure RackAttack en tant que middleware:

config / application.rb

config.middleware.use Rack :: Attack

Conclusion

Nous sommes arrivés à la fin de cet article. Si tout va bien, vous êtes maintenant plus confiant dans la création d’API avec Rails! Notez que ce n’est pas la seule option disponible. Le framework Grape est une autre solution populaire utilisée depuis un certain temps déjà. Vous voudrez peut-être aussi le vérifier..

N'hésitez pas à poster vos questions si quelque chose vous semblait incertain. Je vous remercie de rester avec moi et bon codage!