Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails como API de Single Page Apps

Rails como API de Single Page Apps

Talk apresentada no 18º Encontro Locaweb Recife

willian

May 05, 2016
Tweet

More Decks by willian

Other Decks in Technology

Transcript

  1. @willian "Is a model for providing web and mobile app

    developers with a way to link their applications to backend cloud storage and APIs exposed by back end applications while also providing features such as user management, push notifications, and integration with social networking services." https://en.wikipedia.org/wiki/Mobile_backend_as_a_service
  2. @willian Escrever código do zero leva mais tempo do que

    criar a estrutura dos dados em um BaaS. Prós:
  3. @willian Sair do padrão do BaaS ou precisar de serviços

    que a plataforma escolhida não oferece. Contras:
  4. @willian Garantir que o serviço escolhido terá vida útil maior

    ou igual da sua aplicação. Contras: † 28/01/2017
  5. @willian ...pense bem nos relacionamentos e em como os dados

    serão extraídos para serem usados na aplicação ao criar os modelos. Cuidado...
  6. @willian Caso REAL usando Parse: O sistema lista os bairros

    quando o usuário seleciona a cidade O sistema lista os tipos de cozinhas quando o usuário seleciona o bairro O sistema lista as features de acordo com os filtros selecionados anteriormente
  7. @willian Caso REAL usando Parse (Modelos): City id: String title:

    String slug: String state: String order: Integer numberOfRestaurants: Integer Restaurant id: String city: Relation title: String slug: String neighborhood: String cuisine: String tags: String imagesArray: Array
  8. @willian Problema #1: Restaurant id: String city: Relation title: String

    slug: String neighborhood: String cuisine: String tags: String imagesArray: Array • Neighborhood guarda um valor Plain Text e que se repete em vários registros.
  9. @willian Problema #2: Restaurant id: String city: Relation title: String

    slug: String neighborhood: String cuisine: String tags: String imagesArray: Array • Ao ler o nome do campo entende-se que se trata do nome de uma única cozinha • Armazena diversos nomes de "Cozinhas" em Plain Text, cada nome separado por vírgulas:
 "American,BBQ,Sandwich,Pizza"
  10. @willian Problema #3: Restaurant id: String city: Relation title: String

    slug: String neighborhood: String cuisine: String tags: String imagesArray: Array • Na plataforma, tags são chamadas de "Features". • Novamente um campo Plain Text com itens separados por vírgulas:
 "Breakfast,Lunch,Delivery"
  11. @willian Nessa aplicação, New York tinha cerca de 1242 restaurantes,

    mais do que o limite permitido de resultados por query. Problema #4:
  12. @willian Listar neighborhoods, cuisine ou tags significa percorrer todos os

    1242 restaurantes a cada ação do usuário. Problema #5: Restaurant id: String city: Relation title: String slug: String neighborhood: String cuisine: String tags: String imagesArray: Array
  13. @willian Parse.Cloud .run('findAllRestaurantsForCity', { cityId: cityId }) .then((results) => {

    let restaurants = results.map((restaurant) => { restaurant = self._formatRestaurantAttrs(restaurant) return restaurant }) resolve(restaurants) }, (error) => { reject(error) })
  14. @willian import _ from 'lodash' Parse.Cloud .run('findAllRestaurantsForCity', { cityId: cityId

    }) .then((results) => { let neighborhoods = [] let restaurants = results.map((restaurant) => { restaurant = self._formatRestaurantAttrs(restaurant) return restaurant }) restaurants.forEach((restaurant) => { let neighborhood = { name: restaurant.neighborhoodName, slug: restaurant.neighborhoodSlug } if (!_.find(neighborhoods, "slug", neighborhood.slug)) { neighborhoods.push(neighborhood) } }) neighborhoods = _.sortBy(neighborhoods, "name") resolve(neighborhoods) }, (error) => { reject(error) })
  15. @willian import _ from 'lodash' Parse.Cloud .run('findAllRestaurantsForCity', { cityId: cityId

    }) .then((results) => { let cuisines = [] let restaurants = results.map((restaurant) => { restaurant = self._formatRestaurantAttrs(restaurant) return restaurant }) if (neighborhoodSlug !== "all") { restaurants = _.filter(restaurants, "neighborhoodSlug", neighborhoodSlug) } _.forEach(restaurants, (restaurant) => { _.forEach(restaurant.cuisines, (cuisineName, cuisineSlug) => { if (!_.find(cuisines, "slug", cuisineSlug)) { cuisines.push({ name: cuisineName, slug: cuisineSlug }) } }) }) cuisines = _.sortBy(cuisines, "name") resolve(cuisines); }, (error) => { reject(error) })
  16. @willian Parse.Cloud .run('findAllRestaurantsForCity', { cityId: cityId }) .then((results) => {

    let features = [] let restaurants = results.map((restaurant) => { restaurant = self._formatRestaurantAttrs(restaurant) return restaurant }) if (neighborhoodSlug !== "all") { restaurants = _.filter(restaurants, "neighborhoodSlug", neighborhoodSlug) } if (cuisineSlug !== "all") { restaurants = _.filter(restaurants, (restaurant) => { return restaurant.cuisines[cuisineSlug] }) } _.forEach(restaurants, (restaurant) => { _.forEach(restaurant.features, (featureName, featureSlug) => { if (!_.find(features, "slug", featureSlug)) { features.push({ name: featureName, slug: featureSlug }) } }) }) features = _.sortBy(features, "name") resolve(features) }, (error) => { reject(error) })
  17. @willian Cuisine id: String name: String Features id: String name:

    String Restaurant id: String city: Relation neighborhood: Relation cuisines: Relation features: Relation title: String slug: String imagesArray: Array Possível solução: Neighborhood id: String city: Relation name: String City id: String title: String slug: String state: String order: Integer numberOfRestaurants: Integer
  18. @willian "Usar Rails para cuspir JSON não é overkill? Não

    seria melhor utilizar algo como Sinatra?" Por que usar Rails para APIs JSON?
  19. @willian Rails oferece algumas opções padrões, que podem ser customizadas

    conforme o desejo do desenvolvedor. Por que usar Rails para APIs JSON?
  20. @willian Podemos contar com algumas features mesmo para APIs: •

    Reloading • Development Mode • Test Mode • Logging • Security • Parameter Parsing • Conditional GETs • HEAD requests Por que usar Rails para APIs JSON?
  21. @willian E ainda tem o ActionPack, que nos oferece: •

    Resourceful Routing • URL Generation • Header and Redirection Responses • Caching • Basic, Digest, and Token Authentication • Instrumentation • Generators • Plugins Por que usar Rails para APIs JSON?
  22. @willian Criando uma nova aplicação API-only $ rails new restaurant_api

    --api • Setup limitado de middlewares • Agora ApplicationController herda de ActionController::API • Generators não geram mais views, helpers e assets
  23. @willian Versionamento - HTTP Header Rails.application.routes.draw do api_version(module: "V1", header:

    {name: "Accept", value: "application/json; version=1"}) do get '/cities', to: 'cities#index' end end $ curl -H "Accept: application/json; version=1" http://localhost:3000/cities
  24. @willian Versionamento - Path Rails.application.routes.draw do api_version(module: "V1", path: {value:

    "v1"}) do get '/cities', to: 'cities#index' end end $ curl http://localhost:3000/v1/cities
  25. @willian Versionamento - Request Parameter Rails.application.routes.draw do api_version(module: "V1", parameter:

    {name: "version", value: "v1"}) do get '/cities', to: 'cities#index' end end $ curl http://localhost:3000/cities?version=v1
  26. @willian class UserSerializer < ActiveModel::Serializer attributes :name, :first_name, :last_name, :email

    def first_name object.name.split(" ").first end def last_name object.name.split(" ").last end end ActiveModel::Serializer
  27. @willian ActiveModel::Serializer com JSON API Adapter nativo para JSON API.

    ActiveModel::Serializer.config.adapter = ActiveModelSerializers::Adapter::JsonApi
  28. @willian ActiveModel::Serializer sem JSON API [ { "id":1, "name":"Willian Fernandes",

    "first_name":"Willian", "last_name":"Fernandes", "email":"[email protected]" }, ... ]
  29. @willian ActiveModel::Serializer com JSON API { "data":[ { "id":"1", "type":"users",

    "attributes":{ "name":"Willian Fernandes", "first_name":"Willian", "last_name":"Fernandes", "email":"[email protected]" } }, ... ] }
  30. @willian Segurança User id: String email: String password_digest: String ApiToken

    id: String user_id: Relation encrypted_token: String 1 N
  31. @willian class APIToken < ActiveRecord::Base attr_reader :token belongs_to :user validates

    :user, presence: true before_create :generate_token, unless: :encrypted_token # Encrypt token when setting a new one def token=(new_token) @token = new_token self.encrypted_token = token_digest(@token) if @token.present? end # Check if token is the right one def valid_token?(token) return false if encrypted_token.blank? BCrypt::Password.new(encrypted_token) == token end private # Use bcrypt to encrypt token def token_digest(token) BCrypt::Password.create(token, cost: BCrypt::Engine.cost) end # Generate a token (being careful that it does not exit already) def generate_token send(:token=, SecureRandom.hex) generate_token if self.class.exists?(encrypted_token: encrypted_token) end end
  32. @willian def create @user = User.new(email: params[:email].downcase, password: params[:password], password_confirmation:

    params[:password_confirmation]) @user.save @api_token = @user.api_tokens.create render json: { user: @user, token: @api_token.token } end
  33. @willian class ApplicationController < ActionController::API before_action :authenticate private def authenticate

    @user = authenticate_with_http_token do |email_and_token, _options| email, token = Base64.decode64(email_and_token).split(":") authenticate_user(email, token) || render_unauthorized end end def authenticate_user(email, token) user = User.find_by(email: email) @token = user.try(:valid_token, token) @token && user end def render_unauthorized(message = "Invalid credentials") render( status: :unauthorized, json: { message: "Invalid credentials" } ) end end
  34. @willian require 'test_helper' class UsersTest < ActionDispatch::IntegrationTest test "returns all

    users" do get "/users" assert_response :success refute_empty response.body users = JSON.parse(response.body, symbolize_names: true) assert_equal 2, users.size end end