{"id":13818996,"url":"https://github.com/zayneio/open-flights","last_synced_at":"2025-05-16T04:32:03.001Z","repository":{"id":38007430,"uuid":"241672777","full_name":"zayneio/open-flights","owner":"zayneio","description":"OpenFlights - A CRUD app example built with ruby on rails and react.js using webpacker","archived":false,"fork":false,"pushed_at":"2024-10-31T02:28:33.000Z","size":3477,"stargazers_count":148,"open_issues_count":4,"forks_count":187,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-11-19T18:45:23.906Z","etag":null,"topics":["react","ruby","ruby-on-rails","webpacker"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zayneio.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-02-19T16:56:59.000Z","updated_at":"2024-10-20T14:31:44.000Z","dependencies_parsed_at":"2024-11-19T18:41:06.544Z","dependency_job_id":"24894899-d4ca-4b8b-9edc-ff4eb6877abe","html_url":"https://github.com/zayneio/open-flights","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zayneio%2Fopen-flights","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zayneio%2Fopen-flights/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zayneio%2Fopen-flights/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zayneio%2Fopen-flights/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zayneio","download_url":"https://codeload.github.com/zayneio/open-flights/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254469137,"owners_count":22076440,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","ruby","ruby-on-rails","webpacker"],"created_at":"2024-08-04T08:00:37.207Z","updated_at":"2025-05-16T04:32:02.357Z","avatar_url":"https://github.com/zayneio.png","language":"Ruby","funding_links":[],"categories":["Happy Exploring 🤘"],"sub_categories":[],"readme":"## OpenFlights \n### A flight reviews app built with Ruby on Rails and React.js\n\nThis app is intended to be a simple example of a CRUD app built with **Ruby on Rails** and **React.js** using **Webpacker**.\n\nhttps://github.com/zayneio/open-flights/assets/37857673/489827d5-f142-4064-ba00-48e194acf2c7\n\n\n---\n\n## Running it locally\n- run `rails db:prepare`\n- run `yarn install`\n- run `bundle exec rails s`\n- in another tab run `./bin/webpack-dev-server`\n- in another tab run `sidekiq` (optional, but necessary for things like password reset emails)\n- navigate to `http://localhost:3000`\n\n## Environment Variables\nIf you want functionality like password reset emails to work locally, you'll need to set the following environment variables in `config/application.yml` with your own unique values:\n```\nROOT_URL: http://localhost:3000\nSENDGRID_API_KEY: xxxxxxxxxxxxxx\nSENDGRID_USERNAME: xxxxxxxxxxxxxx\nSENDGRID_PASSWORD: xxxxxxxxxxxxxx\nDEFAULT_FROM_EMAIL: you@example.com\n```\n\n## Routes\n```shell\n             Prefix Verb   URI Pattern                           Controller#Action\n               root GET    /                                     pages#index\n    api_v1_airlines GET    /api/v1/airlines(.:format)            api/v1/airlines#index\n                    POST   /api/v1/airlines(.:format)            api/v1/airlines#create\n new_api_v1_airline GET    /api/v1/airlines/new(.:format)        api/v1/airlines#new\nedit_api_v1_airline GET    /api/v1/airlines/:slug/edit(.:format) api/v1/airlines#edit\n     api_v1_airline GET    /api/v1/airlines/:slug(.:format)      api/v1/airlines#show\n                    PATCH  /api/v1/airlines/:slug(.:format)      api/v1/airlines#update\n                    PUT    /api/v1/airlines/:slug(.:format)      api/v1/airlines#update\n                    DELETE /api/v1/airlines/:slug(.:format)      api/v1/airlines#destroy\n     api_v1_reviews POST   /api/v1/reviews(.:format)             api/v1/reviews#create\n      api_v1_review DELETE /api/v1/reviews/:id(.:format)         api/v1/reviews#destroy\n                    GET    /*path(.:format)                      pages#index\n```\n\n---\n\n## Api V2 (Graphql)\n\n**Get Airlines#index**\n```\nquery Airlines {\n  airlines {\n    id\n    name\n    imageUrl\n    slug\n    averageScore\n    reviews {\n      id\n      title\n      description\n      score\n    }\n  }\n}\n```\n\n**Get Airlines#show**\n```\nquery Airline {\n  airline(slug:) {\n    id\n    name\n    imageUrl\n    slug\n    averageScore\n    reviews {\n      id\n      title\n      description\n      score\n    }\n  }\n}\n```\n\n**Create Review**\n```\nmutation {\n  createReview(\n    title: \"test\",\n    description: \"test\",\n    score: 1,\n    airlineId: 1\n  ) {\n    id\n    title\n    description\n    score\n    airlineId\n    error\n    message\n  }\n}\n```\n\n**Destroy Review**\n```\nmutation {\n  destroyReview(id:) {\n    message\n    error\n  }\n}\n```\n\n---\n\n## How to rebuild this app from scratch (*WORK IN PROGRESS)\n\nFor an up to date, full step-by-step guide on how to rebuild this app from scratch, check out [this article I've put together.](https://zayne.io/articles/how-to-build-a-crud-app-with-ruby-on-rails-and-react)\n\n### Getting Started: Creating a New Rails App With React \u0026 Webpacker\nFirst things first, let's create a brand new rails app. We can do this from the command line by doing `rails new app-name` where app-name is the name of our app, however we are going to add a few additional things. We need to add `--webpack=react` to configure our new app with webpacker to use react, and additionally I'm going to add `--database=postgresql` to configure my app to use postgres as the default database. so the final output to create our new app will look like this:\n\n```shell\nrails new open-flights --webpack=react --database=postgresql\n```\n\nOnce this finishes running, make sure to cd into the directory of your new rails app (`cd open-flights`), then we can go ahead and create the database for our app by entering the following into our command line:\n\n```\nbundle exec rails db:create\n```\n\n## Models\nOur data model for this app will be pretty simple. Our app will have `airlines`, and each airline in our app will have many `reviews`.\n\nFor our airlines, we want to have a `name` for each airline, a unique url-safe `slug`, and an `image_url` for airline logos (Note: I'm not going to handle file uploading in this article, instead we will just link to an image hosted on s3).\n\nFor our reviews, we want to have a `title`, `description`, `score`, and the `airline_id` for the airline the review will belong to. The scoring system I'm going to use for our reviews will be a star rating system that ranges from 1 to 5 stars; 1 being the worst score and 5 being the best score.\n\n\nSo from our command line we can enter the following generators to create our airline and review models in our app:\n\n```shell\nrails g model Airline name slug image_url\n```\n\n```shell\nrails g model Review title description score:integer airline:belongs_to\n```\n\nThis will create two new files in our `db/migrations` folder; one for airlines:\n\n```ruby\nclass CreateAirlines \u003c ActiveRecord::Migration[5.2]\n  def change\n    create_table :airlines do |t|\n      t.string :name\n      t.string :slug\n      t.string :image_url\n\n      t.timestamps\n    end\n  end\nend\n```\n\nand one for reviews:\n\n```ruby\nclass CreateReviews \u003c ActiveRecord::Migration[5.2]\n  def change\n    create_table :reviews do |t|\n      t.string :title\n      t.string :description\n      t.integer :score\n      t.belongs_to :airline, foreign_key: true\n\n      t.timestamps\n    end\n  end\nend\n```\n\nAdditionally, we should now have airline and review model files created for us inside of our `app/models` directory. Because we used `airline:belongs_to` when we generated our review model, our `Review` model should already have the `belongs_to` relationship established, so our this model so far should look like this:\n\n```ruby\nclass Review \u003c ApplicationRecord\n  belongs_to :airline\nend\n```\n\nWe need to additionally add `has_many :reviews` to our airline model. Once we do, our airline model should look like this:\n\n```ruby\nclass Airline \u003c ApplicationRecord\n  has_many :reviews\nend\n```\n\nAt this point, we can go ahead and migrate our database:\n\n```shell\nrails db:migrate\n```\n\nOnce you run that, you should see a new `schema.rb` file created within the `db` folder in our app. Your schema file should now look something like this:\n\n```ruby\nActiveRecord::Schema.define(version: 2019_12_26_200455) do\n  enable_extension \"plpgsql\"\n\n  create_table \"airlines\", force: :cascade do |t|\n    t.string \"name\"\n    t.string \"slug\"\n    t.string \"image_url\"\n    t.datetime \"created_at\", null: false\n    t.datetime \"updated_at\", null: false\n  end\n\n  create_table \"reviews\", force: :cascade do |t|\n    t.string \"title\"\n    t.string \"description\"\n    t.integer \"score\"\n    t.bigint \"airline_id\"\n    t.datetime \"created_at\", null: false\n    t.datetime \"updated_at\", null: false\n    t.index [\"airline_id\"], name: \"index_reviews_on_airline_id\"\n  end\n\n  add_foreign_key \"reviews\", \"airlines\"\nend\n```\n\nSo now for our airline model, we need to do a couple things. First off, I want to add a `before_create` callback method that creates a unique slug based off of the airline's name when we create a new airline. To do this, we can add a new `slugify` method with a before create callback to our airline model like this:\n\n```ruby\nclass Airline \u003c ApplicationRecord\n  has_many :reviews\n\n  before_create :slugify\n\n  def slugify\n    self.slug = name.downcase.gsub(' ', '-')\n  end\nend\n```\n\nThis slugify method will take the name of an airline, convert any uppercase characters to lowercase, replace any spaces with hyphens, and set this value as our slug before saving the record.\n\nActually, I think we can simplify this method further by just calling parameterize on our name attribute instead of using downcase and gsub:\n\n```ruby\nclass Airline \u003c ApplicationRecord\n  has_many :reviews\n\n  before_create :slugify\n\n  def slugify\n    self.slug = name.parameterize\n  end\nend\n```\n\nThis parameterize method should handle both downcasing characters and replacing spaces with hyphens for us. Of course, we can quickly test this out from our rails console to confirm:\n\n```ruby\n'Fake AIRline Name     1'.parameterize\n# =\u003e \"fake-airline-name-1\"\n```\n\nSo now if/when we create a new airline, for example \"United Airlines\", this will convert the name to `united-airlines` and save it as the unique slug for that airline.\n\nAdditionally, we need to create a method that will take all of the reviews that belong to an airline and get the average overall rating. We can add an avg_score method to our model like this:\n\n```ruby\nclass Airline \u003c ApplicationRecord\n  ...\n\n  def avg_score\n    reviews.average(:score).to_f.round(2)\n  end\nend\n```\n\nThis method will return 0 if an airline has no reviews yet. Otherwise it will get the sum of all the review scores for an airline divided by the total number of reviews for that airline to get the average rating.\n\nSo our full Airline model with our slugify method and avg_score method should now look like this:\n\n```ruby\n\nclass Airline \u003c ApplicationRecord\n  has_many :reviews\n\n  before_create :slugify\n\n  def slugify\n    self.slug = name.parameterize\n  end\n\n  def avg_score\n    reviews.average(:score).to_f.round(2)\n  end\nend\n```\n\n\n## Seeding Our Database\nNow that we have got our models created, let's go ahead and seed our database with some data! We can add this to the `seeds.rb` file located inside of our `db` folder:\n\n```ruby\nAirline.create([\n  { \n    name: \"United Airlines\",\n    image_url: \"https://open-flights.s3.amazonaws.com/United-Airlines.png\"\n  }, \n  { \n    name: \"Southwest\",\n    image_url: \"https://open-flights.s3.amazonaws.com/Southwest-Airlines.png\"\n  },\n  { \n    name: \"Delta\",\n    image_url: \"https://open-flights.s3.amazonaws.com/Delta.png\" \n  }, \n  { \n    name: \"Alaska Airlines\",\n    image_url: \"https://open-flights.s3.amazonaws.com/Alaska-Airlines.png\" \n  }, \n  { \n    name: \"JetBlue\",\n    image_url: \"https://open-flights.s3.amazonaws.com/JetBlue.png\" \n  }, \n  { \n    name: \"American Airlines\",\n    image_url: \"https://open-flights.s3.amazonaws.com/American-Airlines.png\" \n  }\n])\n```\n\nAnd then we can seed our database by running the following command in our terminal:\n\n```shell\nrails db:seed\n```\n\nNow if we jump into our rails console with `rails c` we should be able to see our new data in the database:\n\n```ruby\nAirline.first\n# =\u003e #\u003cAirline id: 1, name: \"United Airlines\", slug: \"united-airlines\", image_url: \"https://open-flights.s3.amazonaws.com/United-Airlines.png\", created_at: \"2019-12-26 23:02:58\", updated_at: \"2019-12-26 23:02:58\"\u003e\n```\n\nNotice that even though we only included the name and image_url in our seed data, we additionally have a slug value (in this case \"united-airlines\") because we added that slugify method to our airline model. We will use this slug shortly as the paramater to find records by in our controllers, instead of using the id param.\n\n\n## Serializers: Building Our JSON API\n\nFor this app we are going to use [fast_jsonapi](https://github.com/Netflix/fast_jsonapi), a gem created by the Netflix engineering team. If you have ever used Active Model Serializer (AMS), you will likely notice some similarities.\n\nwith fast_jsonapi, we can create the exact structure for the data we want to expose in our api, and then use that when we render json from within our controllers.\n\nLet's install the fast_jsonapi gem, by adding it to our Gemfile:\n\n```ruby\ngem 'fast_jsonapi'\n```\n\nThen we can install it with bundle install from our terminal:\n\n```shell\nbundle install\n```\n\nNow we can use a generator to create a new airline serializer and review serializer, passing along the specific attributes we want to expose in our api:\n\n```shell\nrails g serializer Airline name slug image_url\n```\n\n```shell\nrails g serializer Review title description score airline_id\n```\n\nThis will create a new serializer folder in our app and create a new airline serializer that should so far look like this:\n\n```ruby\nclass AirlineSerializer\n  include FastJsonapi::ObjectSerializer\n  attributes :name, :slug, :image_url\nend\n```   \n\nAnd a reviews serializer that should look like this:\n\n```ruby\nclass ReviewSerializer\n  include FastJsonapi::ObjectSerializer\n  attributes :title, :description, :score, :airline_id\nend  \n```\n\nFor our airlines serializer, we want to include the relationship with reviews in our serialized json. We can add this simply by adding `has_many :reviews` into our serializer. So then our serializer should look like this:\n\n```ruby\nclass AirlineSerializer\n  include FastJsonapi::ObjectSerializer\n  attributes :name, :slug, :image_url\n  has_many :reviews\nend\n```\n\nLet's take a quick look at how we can use our serializers now to structure our api. If we jump into a rails console (`rails c`) in our terminal, let's get the first airline from our database. Then we can initialize a new instance of our airline serializer with that record and return the result as serialized json:\n\n```ruby\n# Get the first airline record from our database\nairline = Airline.first\n=\u003e #\u003cAirline id: 1, name: \"United Airlines\", slug: \"united-airlines\", image_url: \"https://open-flights.s3.amazonaws.com/United-Airlines.png\", created_at: \"2019-12-26 23:02:58\", updated_at: \"2019-12-26 23:02:58\"\u003e\n\n# Serialized JSON\nAirlineSerializer.new(airline).serialized_json\n=\u003e \"{\\\"data\\\":{\\\"id\\\":\\\"1\\\",\\\"type\\\":\\\"airline\\\",\\\"attributes\\\":{\\\"name\\\":\\\"United Airlines\\\",\\\"slug\\\":\\\"united-airlines\\\",\\\"image_url\\\":\\\"https://open-flights.s3.amazonaws.com/United-Airlines.png\\\"},\\\"relationships\\\":{\\\"reviews\\\":{\\\"data\\\":[]}}}}\"\n\n# Formatted JSON\nAirlineSerializer.new(airline).as_json\n=\u003e {\n  \"data\" =\u003e {\n    \"id\" =\u003e \"1\", \n    \"type\" =\u003e \"airline\", \n    \"attributes\" =\u003e  {\n      \"name\" =\u003e \"United Airlines\", \n      \"slug\" =\u003e \"united-airlines\", \n      \"image_url\" =\u003e \"https://open-flights.s3.amazonaws.com/United-Airlines.png\"\n    }, \n    \"relationships\" =\u003e {\n      \"reviews\" =\u003e {\n        \"data\" =\u003e []\n      }\n    }\n  }\n}\n```\n\nIn the above examples, you can see that the only attributes shared within the attributes section are those that we have explicitly declared in our airline seriaizer.\n\n## Controllers\nOur app is going to have three controllers: an airlines controller, a reviews controller and a pages controller. Our pages controller will have a single index action that I'm going to use as the root path of our app. I'm also going to use Pages#index as a sort of catch-all for any requests outside of our api. This will come in handy once we start using react-router in a little, as we will need to be able to match routes to different components.\n\nFor our airlines and reviews controllers, we are going to namespace everything under api/v1. Again, this will give us an easy way to manage routing from both the react side of our app and the rails side once we additionally start using react-router in a moment.\n\nFor example, if a user navigates to /airlines in our app, on the react side we can load the necessary components to show a list of all airlines, and on the back end we can make the request to our Airline#index action in our controller as /api/v1/airlines to get a list of all of the airlines from our api.\n\n### Routes\nLet's actually go ahead and set up our routes, adding our root path and our namespaced api resources:\n\n```ruby\nRails.application.routes.draw do\n\n  root 'pages#index'\n\n  namespace :api do\n    namespace :v1 do\n      resources :airlines, param: :slug\n      resources :reviews, only: [:create, :destroy]\n    end\n  end\n\n  get '*path', to: 'pages#index', via: :all\nend   \n```\n\nNotice that I added `param: :slug` to our airlines resources so that we can use our slugs as the primary param for airlines instead of using id.\n\n### Airlines Controller\nInside of `app/controllers`, let's create a new `api` folder, and inside of that, a new `v1` folder, and then inside of that let's create a new airlines controller, namespaced under`Api::V1`:\n\n```ruby\nmodule Api\n  module V1\n    class AirlinesController \u003c ApplicationController\n    end\n  end\nend\n```\n\n### Airlines#index\nNow let's add an index method to our new controller. All we need to do for this method is get all of the airlines from our database, then render the data as JSON using our AirlineSerializer.\n\nTo get all of our airlines, we can simply call all on our Airline model like so:\n```ruby\nairlines = Airline.all\n```\n\nThen we can pass our airlines variable as an argument into a new instance of our AirlineSerializer and return our data as serialized JSON like so:\n\n```ruby\nAirlineSerializer.new(airlines).serialized_json\n```\n\nSo putting these two steps together, and then rendering the result as JSON from our controller, our index method should look like this:\n\n```ruby\nmodule Api\n  module V1\n    class AirlinesController \u003c ApplicationController\n      def index\n        airlines = Airline.all\n\n        render json: AirlineSerializer.new(airlines).serialized_json\n      end\n    end\n  end\nend\n```\n\n### Airlines#show\nOur show method will also be pretty simple. For this we just need to find a specific airline, not by its id, but using it's slug as the param. We can do this by calling find_by on our Airline model and searching for a record that has a matching slug, like so:\n\n```ruby\nairline = Airline.find_by(slug: params[:slug])\n```\n\nThen, we will again render the resulting JSON using our AirlineSerializer. So our show method should look like this:\n\n```ruby\nmodule Api\n  module V1\n    class AirlinesController \u003c ApplicationController\n      ...\n      \n      def show\n        airline = Airline.find_by(slug: params[:slug])\n\n        render json: AirlineSerializer.new(airlines).serialized_json\n      end\n    end\n  end\nend\n```\n\n### Airlines#create\nBefore we add our create method, let's use strong paramaters to create a whitelist of allowed parameters when creating a new airline in our app. For now we will allow only `name` and `image_url`:\n\n```ruby\nmodule Api\n  module V1\n    class AirlinesController \u003c ApplicationController\n      \n      ... \n\n      private\n\n      def airline_params\n        params.require(:airline).permit(:name, :image_url)\n      end\n    end\n  end\nend\n```\n\nThen we can go ahead and add our create method. For this, we will simply initialize a new instance of Airline, passing in our airline_params. If everything is valid and saves, we will render data for our new airline again using our airline serializer, otherwise we will return an error:\n\n```ruby\n\nmodule Api\n  module V1\n    class AirlinesController \u003c ApplicationController\n\n      ...\n\n      def create\n        airline = Airline.new(airline_params)\n\n        if airline.save\n          render json: AirlineSerializer.new(airline).serialized_json\n        else\n          render json: { error: airline.errors.messages }, status: 422\n        end\n      end\n\n      private\n\n      def airline_params\n        params.require(:airline).permit(:name, :image_url)\n      end\n    end\n  end\nend\n```\n\n---\n\nThis README is still being written - check back soon!\n\n---\n\n## License\n```\nCopyright (c) 2020 zayneio\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzayneio%2Fopen-flights","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzayneio%2Fopen-flights","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzayneio%2Fopen-flights/lists"}