One place for hosting & domains

      Build a RESTful JSON API With Rails 5 – Part Three

      Introduction

      In part two of this tutorial, we added token-based authentication with JWT (JSON Web Tokens) to our todo API.

      In this final part of the series, we’ll wrap with the following:

      • Versioning
      • Serializers
      • Pagination

      When building an API whether public or internal facing, it’s highly recommended that you version it.
      This might seem trivial when you have total control over all clients. However, when the API is public-facing, you want to establish a contract with your clients. Every breaking change should be a new version. Convincing enough? Great, let’s do this!

      In order to version a Rails API, we need to do two things:

      1. Add a route constraint – this will select a version based on the request headers
      2. Namespace the controllers – have different controller namespaces to handle different versions.

      Rails routing supports advanced constraints. Provided an object that responds to matches?, you can control which controller handles a specific route.

      We’ll define a class ApiVersion that checks the API version from the request headers and routes to the appropriate controller module. The class will live in app/lib since it’s non-domain-specific.

      1. touch app/lib/api_version.rb

      Implement ApiVersion

      app/lib/api_version.rb

      class ApiVersion
        attr_reader :version, :default
      
        def initialize(version, default = false)
          @version = version
          @default = default
        end
      
        
        def matches?(request)
          check_headers(request.headers) || default
        end
      
        private
      
        def check_headers(headers)
          
          accept = headers[:accept]
          accept && accept.include?("application/vnd.todos.#{version}+json")
        end
      end
      

      The ApiVersion class accepts a version and a default flag on initialization. In accordance with Rails constraints, we implement an instance method matches?. This method will be called with the request object upon initialization.

      From the request object, we can access the Accept headers and check for the requested version or if the instance is the default version. This process is called content negotiation. Let’s add some more context to this.

      Content Negotiation

      REST is closely tied to the HTTP specification. HTTP defines mechanisms that make it possible to serve different versions (representations) of a resource at the same URI. This is called content negotiation.

      Our ApiVersion class implements server-driven content negotiation where the client (user agent) informs the server what media types it understands by providing an Accept HTTP header.

      According to the Media Type Specification, you can define your own media types using the vendor tree i.e., application/vnd.example.resource+json.

      The vendor tree is used for media types associated with publicly available products. It uses the “vnd” facet.

      Thus, we define a custom vendor media type application/vnd.todos.{version_number}+json giving clients the ability to choose which API version they require.

      Cool, now that we have the constraint class, let’s change our routing to accommodate this.

      Since we don’t want to have the version number as part of the URI (this is argued as an anti-pattern), we’ll make use of the module scope to namespace our controllers.

      Let’s move the existing todos and todo-items resources into a v1 namespace.

      config/routes

      Rails.application.routes.draw do
        
      
        
        scope module: :v1, constraints: ApiVersion.new('v1', true) do
          resources :todos do
            resources :items
          end
        end
      
      
        post 'auth/login', to: 'authentication#authenticate'
        post 'signup', to: 'users#create'
      end
      

      We’ve set the version constraint at the namespace level. Thus, this will be applied to all resources within it. We’ve also defined v1 as the default version; in cases where the version is not provided, the API will default to v1.

      In the event we were to add new versions, they would have to be defined above the default version since Rails will cycle through all routes from top to bottom searching for one that matches(till method matches? resolves to true).

      Next up, let’s move the existing todos and items controllers into the v1 namespace. First, create a module directory in the controllers folder.

      1. mkdir app/controllers/v1

      Move the files into the module folder.

      1. mv app/controllers/{todos_controller.rb,items_controller.rb} app/controllers/v1

      That’s not all, let’s define the controllers in the v1 namespace. Let’s start with the todos controller.

      app/controllers/v1/todos_controller.rb

      module V1
        class TodosController < ApplicationController
        
        end
      end
      

      Do the same for the items controller.

      app/controllers/v1/items_controller.rb

      module V1
        class ItemsController < ApplicationController
        
        end
      end
      

      Let’s fire up the server and run some tests.

      1. http :3000/auth/login email=[email protected] password=foobar
      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'
      1. http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'ey...AWH3FNTd3T0jMB7HnLw2bYQbK0g'

      In case we attempt to access a nonexistent version, the API will default to v1 since we set it as the default version. For testing purposes, let’s define v2.

      Generate a v2 todos controller

      1. rails g controller v2/todos

      Define the namespace in the routes.

      config/routes.rb

      Rails.application.routes.draw do
        
      
        
        scope module: :v2, constraints: ApiVersion.new('v2') do
          resources :todos, only: :index
        end
      
        scope module: :v1, constraints: ApiVersion.new('v1', true) do
          
        end
        
      end
      

      Remember, non-default versions have to be defined above the default version.

      Since this is test controller, we’ll define an index controller with a dummy response.

      app/controllers/v2/todos_controller.rb

      class V2::TodosController < ApplicationController
        def index
          json_response({ message: 'Hello there'})
        end
      end
      

      Note the namespace syntax, this is shorthand in Ruby to define a class within a namespace.
      Great, now fire up the server once more and run some tests.

      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0e...Lw2bYQbK0g'
      1. http :3000/todos Accept:'application/vnd.todos.v2+json' Authorization:'eyJ0e...Lw2bYQbK0g'

      Voila! Our API responds to version 2!

      At this point, if we wanted to get a todo and its items, we’d have to make two API calls.
      Although this works well, it’s not ideal.

      We can achieve this with serializers. Serializers allow for custom representations of JSON responses. Active model serializers make it easy to define which model attributes and relationships need to be serialized. In order to get todos with their respective items, we need to define serializers on the Todo model to include its attributes and relationships.

      First, let’s add active model serializers to the Gemfile:

      Gemfile

      
        gem 'active_model_serializers', '~> 0.10.0'
      
      

      Run bundle to install it:

      1. bundle install

      Generate a serializer from the todo model:

      1. rails g serializer todo

      This creates a new directory app/serializers and adds a new file todo_serializer.rb. Let’s define the todo serializer with the data that we want it to contain.

      app/serializers/todo_serializer.rb

      class TodoSerializer < ActiveModel::Serializer
        
        attributes :id, :title, :created_by, :created_at, :updated_at
        
        has_many :items
      end
      

      We define a whitelist of attributes to be serialized and the model association (only defined attributes will be serialized). We’ve also defined a model association to the item model, this way the payload will include an array of items. Fire up the server, let’s test this.

      1. http POST :3000/todos/1/items name='Listen to Don Giovanni' Accept:'application/vnd.todos.v1+json' Authorization:'ey...HnLw2bYQbK0g'
      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'ey...HnLw2bYQbK0g'

      This is great. One request to rule them all!

      Our todos API has suddenly become very popular. All of a sudden everyone has something to do. Our data set has grown substantially. To make sure the requests are still fast and optimized, we’re going to add pagination; we’ll give clients the power to say what portion of data they require.

      To achieve this, we’ll make use of the will_paginate gem.

      Let’s add it to the Gemfile:

      Gemfile

      
        gem 'will_paginate', '~> 3.1.0'
      
      

      Install it:

      1. bundle install

      Let’s modify the todos controller index action to paginate its response.

      app/controllers/v1/todos_controller.rb

      module V1
        class TodosController < ApplicationController
        
        
        def index
          
          @todos = current_user.todos.paginate(page: params[:page], per_page: 20)
          json_response(@todos)
        end
        
      end
      

      The index action checks for the page number in the request params. If provided, it’ll return the page data with each page having twenty records each. As always, let’s fire up the Rails server and run some tests.

      1. http :3000/todos Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
      1. http :3000/todos page==1 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'
      1. http :3000/todos page==2 Accept:'application/vnd.todos.v1+json' Authorization:'eyJ0...nLw2bYQbK0g'

      The page number is part of the query string. Note that when we request the second page, we get an empty array. This is because we don’t have more than 20 records in the database.

      Let’s seed some test data into the database.

      Add faker and install faker gem. Faker generates data at random.

      Gemfile

      
        gem 'faker'
      
      

      In db/seeds.rb let’s define seed data.

      db/seeds.rb

      
      50.times do
        todo = Todo.create(title: Faker::Lorem.word, created_by: User.first.id)
        todo.items.create(name: Faker::Lorem.word, done: false)
      end
      

      Seed the database by running:

      1. rake db:seed

      Awesome, fire up the server and rerun the HTTP requests. Since we have test data, we’re able to see data from different pages.

      Congratulations for making it this far! We’ve come a long way! We’ve gone through generating an API-only Rails application, setting up a test framework, using TDD to implement the todo API, adding token-based authentication with JWT, versioning our API, serializing with active model serializers, and adding pagination features.

      Having gone through this series, I believe you should be able to build a RESTful API with Rails 5. Feel free to leave any feedback you may have in the comments section below. If you found the tutorial helpful, don’t hesitate to hit that share button. Cheers!

      Working with JSON in MySQL


      Introduction

      SQL databases tend to be rigid.

      If you have worked with them, you would agree that database design though it seems easier, is a lot trickier in practice. SQL databases believe in structure, that is why it’s called structured query language.

      On the other side of the horizon, we have the NoSQL databases, also called schema-less databases that encourage flexibility. In schema-less databases, there is no imposed structural restriction, only data to be saved.

      Though every tool has it’s use case, sometimes things call for a hybrid approach.

      What if you could structure some parts of your database and leave others to be flexible?

      MySQL version 5.7.8 introduces a JSON data type that allows you to accomplish that.

      In this tutorial, you are going to learn.

      1. How to design your database tables using JSON fields.
      2. The various JSON based functions available in MYSQL to create, read, update, and delete rows.
      3. How to work with JSON fields using the Eloquent ORM in Laravel.

      Why Use JSON

      At this moment, you are probably asking yourself why would you want to use JSON when MySQL has been catering to a wide variety of database needs even before it introduced a JSON data type.

      The answer lies in the use-cases where you would probably use a make-shift approach.

      Let me explain with an example.

      Suppose you are building a web application where you have to save a user’s configuration/preferences in the database.

      Generally, you can create a separate database table with the id, user_id, key, and value fields or save it as a formatted string that you can parse at runtime.

      However, this works well for a small number of users. If you have about a thousand users and five configuration keys, you are looking at a table with five thousand records that addresses a very small feature of your application.

      Or if you are taking the formatted string route, extraneous code that only compounds your server load.

      Using a JSON data type field to save a user’s configuration in such a scenario can spare you a database table’s space and bring down the number of records, which were being saved separately, to be the same as the number of users.

      And you get the added benefit of not having to write any JSON parsing code, the ORM or the language runtime takes care of it.

      The Schema

      Before we dive into using all the cool JSON stuff in MySQL, we are going to need a sample database to play with.

      So, let’s get our database schema out of the way first.

      We are going to consider the use case of an online store that houses multiple brands and a variety of electronics.

      Since different electronics have different attributes(compare a Macbook with a Vacuumn Cleaner) that buyers are interested in, typically the Entity–attribute–value model (EAV) pattern is used.

      However, since we now have the option to use a JSON data type, we are going to drop EAV.

      For a start, our database will be named e_store and has three tables only named, brands, categories, and products respectively.

      Our brands and categories tables will be pretty similar, each having an id and a name field.

      CREATE DATABASE IF NOT EXISTS `e_store`
      DEFAULT CHARACTER SET utf8
      DEFAULT COLLATE utf8_general_ci;
      
      SET default_storage_engine = INNODB;
      
      CREATE TABLE `e_store`.`brands`(
          `id` INT UNSIGNED NOT NULL auto_increment ,
          `name` VARCHAR(250) NOT NULL ,
          PRIMARY KEY(`id`)
      );
      
      CREATE TABLE `e_store`.`categories`(
          `id` INT UNSIGNED NOT NULL auto_increment ,
          `name` VARCHAR(250) NOT NULL ,
          PRIMARY KEY(`id`)
      );
      

      The objective of these two tables will be to house the product categories and the brands that provide these products.

      While we are at it, let us go ahead and seed some data into these tables to use later.

      /* Brands */
      INSERT INTO `e_store`.`brands`(`name`)
      VALUES
          ('Samsung');
      
      INSERT INTO `e_store`.`brands`(`name`)
      VALUES
          ('Nokia');
      
      INSERT INTO `e_store`.`brands`(`name`)
      VALUES
          ('Canon');
      
      /* Types of electronic device */
      INSERT INTO `e_store`.`categories`(`name`)
      VALUES
          ('Television');
      
      INSERT INTO `e_store`.`categories`(`name`)
      VALUES
          ('Mobilephone');
      
      INSERT INTO `e_store`.`categories`(`name`)
      VALUES
          ('Camera');
      

      The brands table

      The categories table

      Next, is the business area of this tutorial.

      We are going to create a products table with the id, name, brand_id, category_id, and attributes fields.

      CREATE TABLE `e_store`.`products`(
          `id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
          `name` VARCHAR(250) NOT NULL ,
          `brand_id` INT UNSIGNED NOT NULL ,
          `category_id` INT UNSIGNED NOT NULL ,
          `attributes` JSON NOT NULL ,
          PRIMARY KEY(`id`) ,
          INDEX `CATEGORY_ID`(`category_id` ASC) ,
          INDEX `BRAND_ID`(`brand_id` ASC) ,
          CONSTRAINT `brand_id` FOREIGN KEY(`brand_id`) REFERENCES `e_store`.`brands`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE ,
          CONSTRAINT `category_id` FOREIGN KEY(`category_id`) REFERENCES `e_store`.`categories`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE
      );
      

      Our table definition specifies foreign key constraints for the brand_id and category_id fields, specifying that they reference the brands and categories table respectively. We have also specified that the referenced rows should not be allowed to delete and if updated, the changes should reflect in the references as well.

      The attributes field’s column type has been declared to be JSON which is the native data type now available in MySQL. This allows us to use the various JSON related constructs in MySQL on our attributes field.

      Here is an entity relationship diagram of our created database.

      The e_store database

      Our database design is not the best in terms of efficiency and accuracy. There is no price column in the products table and we could do with putting a product into multiple categories. However, the purpose of this tutorial is not to teach database design but rather how to model objects of different nature in a single table using MySQL’s JSON features.

      The CRUD Operations

      Let us look at how to create, read, update, and delete data in a JSON field.

      Create

      Creating a record in the database with a JSON field is pretty simple.

      All you need to do is add valid JSON as the field value in your insert statement.

      /* Let's sell some televisions */
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Prime' ,
          '1' ,
          '1' ,
          '{"screen": "50 inch", "resolution": "2048 x 1152 pixels", "ports": {"hdmi": 1, "usb": 3}, "speakers": {"left": "10 watt", "right": "10 watt"}}'
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Octoview' ,
          '1' ,
          '1' ,
          '{"screen": "40 inch", "resolution": "1920 x 1080 pixels", "ports": {"hdmi": 1, "usb": 2}, "speakers": {"left": "10 watt", "right": "10 watt"}}'
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Dreamer' ,
          '1' ,
          '1' ,
          '{"screen": "30 inch", "resolution": "1600 x 900 pixles", "ports": {"hdmi": 1, "usb": 1}, "speakers": {"left": "10 watt", "right": "10 watt"}}'
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Bravia' ,
          '1' ,
          '1' ,
          '{"screen": "25 inch", "resolution": "1366 x 768 pixels", "ports": {"hdmi": 1, "usb": 0}, "speakers": {"left": "5 watt", "right": "5 watt"}}'
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Proton' ,
          '1' ,
          '1' ,
          '{"screen": "20 inch", "resolution": "1280 x 720 pixels", "ports": {"hdmi": 0, "usb": 0}, "speakers": {"left": "5 watt", "right": "5 watt"}}'
      );
      

      The products table after adding televisions

      Instead of laying out the JSON object yourself, you can also use the built-in JSON_OBJECT function.

      The JSON_OBJECT function accepts a list of key/value pairs in the form JSON_OBJECT(key1, value1, key2, value2, ... key(n), value(n)) and returns a JSON object.

      /* Let's sell some mobilephones */
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Desire' ,
          '2' ,
          '2' ,
          JSON_OBJECT(
              "network" ,
              JSON_ARRAY("GSM" , "CDMA" , "HSPA" , "EVDO") ,
              "body" ,
              "5.11 x 2.59 x 0.46 inches" ,
              "weight" ,
              "143 grams" ,
              "sim" ,
              "Micro-SIM" ,
              "display" ,
              "4.5 inches" ,
              "resolution" ,
              "720 x 1280 pixels" ,
              "os" ,
              "Android Jellybean v4.3"
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Passion' ,
          '2' ,
          '2' ,
          JSON_OBJECT(
              "network" ,
              JSON_ARRAY("GSM" , "CDMA" , "HSPA") ,
              "body" ,
              "6.11 x 3.59 x 0.46 inches" ,
              "weight" ,
              "145 grams" ,
              "sim" ,
              "Micro-SIM" ,
              "display" ,
              "4.5 inches" ,
              "resolution" ,
              "720 x 1280 pixels" ,
              "os" ,
              "Android Jellybean v4.3"
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Emotion' ,
          '2' ,
          '2' ,
          JSON_OBJECT(
              "network" ,
              JSON_ARRAY("GSM" , "CDMA" , "EVDO") ,
              "body" ,
              "5.50 x 2.50 x 0.50 inches" ,
              "weight" ,
              "125 grams" ,
              "sim" ,
              "Micro-SIM" ,
              "display" ,
              "5.00 inches" ,
              "resolution" ,
              "720 x 1280 pixels" ,
              "os" ,
              "Android KitKat v4.3"
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Sensation' ,
          '2' ,
          '2' ,
          JSON_OBJECT(
              "network" ,
              JSON_ARRAY("GSM" , "HSPA" , "EVDO") ,
              "body" ,
              "4.00 x 2.00 x 0.75 inches" ,
              "weight" ,
              "150 grams" ,
              "sim" ,
              "Micro-SIM" ,
              "display" ,
              "3.5 inches" ,
              "resolution" ,
              "720 x 1280 pixels" ,
              "os" ,
              "Android Lollypop v4.3"
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Joy' ,
          '2' ,
          '2' ,
          JSON_OBJECT(
              "network" ,
              JSON_ARRAY("CDMA" , "HSPA" , "EVDO") ,
              "body" ,
              "7.00 x 3.50 x 0.25 inches" ,
              "weight" ,
              "250 grams" ,
              "sim" ,
              "Micro-SIM" ,
              "display" ,
              "6.5 inches" ,
              "resolution" ,
              "1920 x 1080 pixels" ,
              "os" ,
              "Android Marshmallow v4.3"
          )
      );
      

      The products table after adding mobilephones

      Notice the JSON_ARRAY function which returns a JSON array when passed a set of values.

      If you specify a single key multiple times, only the first key/value pair will be retained. This is called normalizing the JSON in MySQL’s terms. Also, as part of normalization, the object keys are sorted and the extra white-space between key/value pairs is removed.

      Another function that we can use to create JSON objects is the JSON_MERGE function.

      The JSON_MERGE function takes multiple JSON objects and produces a single, aggregate object.

      /* Let's sell some cameras */
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Explorer' ,
          '3' ,
          '3' ,
          JSON_MERGE(
              '{"sensor_type": "CMOS"}' ,
              '{"processor": "Digic DV III"}' ,
              '{"scanning_system": "progressive"}' ,
              '{"mount_type": "PL"}' ,
              '{"monitor_type": "LCD"}'
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Runner' ,
          '3' ,
          '3' ,
          JSON_MERGE(
              JSON_OBJECT("sensor_type" , "CMOS") ,
              JSON_OBJECT("processor" , "Digic DV II") ,
              JSON_OBJECT("scanning_system" , "progressive") ,
              JSON_OBJECT("mount_type" , "PL") ,
              JSON_OBJECT("monitor_type" , "LED")
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Traveler' ,
          '3' ,
          '3' ,
          JSON_MERGE(
              JSON_OBJECT("sensor_type" , "CMOS") ,
              '{"processor": "Digic DV II"}' ,
              '{"scanning_system": "progressive"}' ,
              '{"mount_type": "PL"}' ,
              '{"monitor_type": "LCD"}'
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Walker' ,
          '3' ,
          '3' ,
          JSON_MERGE(
              '{"sensor_type": "CMOS"}' ,
              '{"processor": "Digic DV I"}' ,
              '{"scanning_system": "progressive"}' ,
              '{"mount_type": "PL"}' ,
              '{"monitor_type": "LED"}'
          )
      );
      
      INSERT INTO `e_store`.`products`(
          `name` ,
          `brand_id` ,
          `category_id` ,
          `attributes`
      )
      VALUES(
          'Jumper' ,
          '3' ,
          '3' ,
          JSON_MERGE(
              '{"sensor_type": "CMOS"}' ,
              '{"processor": "Digic DV I"}' ,
              '{"scanning_system": "progressive"}' ,
              '{"mount_type": "PL"}' ,
              '{"monitor_type": "LCD"}'
          )
      );
      

      The products table after adding cameras

      There is a lot happening in these insert statements and it can get a bit confusing. However, it is pretty simple.

      We are only passing objects to the JSON_MERGE function. Some of them have been constructed using the JSON_OBJECT function we saw previously whereas others have been passed as valid JSON strings.

      In case of the JSON_MERGE function, if a key is repeated multiple times, it’s value is retained as an array in the output.

      A proof of concept is in order I suppose.

      /* output: {"network": ["GSM", "CDMA", "HSPA", "EVDO"]} */
      SELECT JSON_MERGE(
          '{"network": "GSM"}' ,
          '{"network": "CDMA"}' ,
          '{"network": "HSPA"}' ,
          '{"network": "EVDO"}'
      );
      

      We can confirm all our queries were run successfully using the JSON_TYPE function which gives us the field value type.

      /* output: OBJECT */
      SELECT JSON_TYPE(attributes) FROM `e_store`.`products`;
      

      Add attributes are JSON objects

      Read

      Right, we have a few products in our database to work with.

      For typical MySQL values that are not of type JSON, a where clause is pretty straight-forward. Just specify the column, an operator, and the values you need to work with.

      Heuristically, when working with JSON columns, this does not work.

      /* It's not that simple */
      SELECT
          *
      FROM
          `e_store`.`products`
      WHERE
          attributes="{"ports": {"usb": 3, "hdmi": 1}, "screen": "50 inch", "speakers": {"left": "10 watt", "right": "10 watt"}, "resolution": "2048 x 1152 pixels"}";
      

      When you wish to narrow down rows using a JSON field, you should be familiar with the concept of a path expression.

      The most simplest definition of a path expression(think JQuery selectors) is it’s used to specify which parts of the JSON document to work with.

      The second piece of the puzzle is the JSON_EXTRACT function which accepts a path expression to navigate through JSON.

      Let us say we are interested in the range of televisions that have atleast a single USB and HDMI port.

      SELECT
          *
      FROM
          `e_store`.`products`
      WHERE
          `category_id` = 1
      AND JSON_EXTRACT(`attributes` , '$.ports.usb') > 0
      AND JSON_EXTRACT(`attributes` , '$.ports.hdmi') > 0;
      

      Selecting records by JSON attributes

      The first argument to the JSON_EXTRACT function is the JSON to apply the path expression to which is the attributes column. The $ symbol tokenizes the object to work with. The $.ports.usb and $.ports.hdmi path expressions translate to “take the usb key under ports” and “take the hdmi key under ports” respectively.

      Once we have extracted the keys we are interested in, it is pretty simple to use the MySQL operators such as > on them.

      Also, the JSON_EXTRACT function has the alias -> that you can use to make your queries more readable.

      Revising our previous query.

      SELECT
          *
      FROM
          `e_store`.`products`
      WHERE
          `category_id` = 1
      AND `attributes` -> '$.ports.usb' > 0
      AND `attributes` -> '$.ports.hdmi' > 0;
      

      Update

      In order to update JSON values, we are going to use the JSON_INSERT, JSON_REPLACE, and JSON_SET functions. These functions also require a path expression to specify which parts of the JSON object to modify.

      The output of these functions is a valid JSON object with the changes applied.

      Let us modify all mobilephones to have a chipset property as well.

      UPDATE `e_store`.`products`
      SET `attributes` = JSON_INSERT(
          `attributes` ,
          '$.chipset' ,
          'Qualcomm'
      )
      WHERE
          `category_id` = 2;
      

      Updated mobilephones

      The $.chipset path expression identifies the position of the chipset property to be at the root of the object.

      Let us update the chipset property to be more descriptive using the JSON_REPLACE function.

      UPDATE `e_store`.`products`
      SET `attributes` = JSON_REPLACE(
          `attributes` ,
          '$.chipset' ,
          'Qualcomm Snapdragon'
      )
      WHERE
          `category_id` = 2;
      

      Updated mobilephones

      Easy peasy!

      Lastly, we have the JSON_SET function which we will use to specify our televisions are pretty colorful.

      UPDATE `e_store`.`products`
      SET `attributes` = JSON_SET(
          `attributes` ,
          '$.body_color' ,
          'red'
      )
      WHERE
          `category_id` = 1;
      

      Updated televisions

      All of these functions seem identical but there is a difference in the way they behave.

      The JSON_INSERT function will only add the property to the object if it does not exists already.

      The JSON_REPLACE function substitutes the property only if it is found.

      The JSON_SET function will add the property if it is not found else replace it.

      Delete

      There are two parts to deleting that we will look at.

      The first is to delete a certain key/value from your JSON columns whereas the second is to delete rows using a JSON column.

      Let us say we are no longer providing the mount_type information for cameras and wish to remove it for all cameras.

      We will do it using the JSON_REMOVE function which returns the updated JSON after removing the specified key based on the path expression.

      UPDATE `e_store`.`products`
      SET `attributes` = JSON_REMOVE(`attributes` , '$.mount_type')
      WHERE
          `category_id` = 3;
      

      Cameras after removing mount_type property

      For the second case, we also do not provide mobilephones anymore that have the Jellybean version of the Android OS.

      DELETE FROM `e_store`.`products`
      WHERE `category_id` = 2
      AND JSON_EXTRACT(`attributes` , '$.os') LIKE '%Jellybean%';
      

      We do not sell Jellybeans anymore!

      As stated previously, working with a specific attribute requires the use of the JSON_EXTRACT function so in order to apply the LIKE operator, we have first extracted the os property of mobilephones(with the help of category_id) and deleted all records that contain the string Jellybean.

      A Primer for Web Applications

      The old days of directly working with a database are way behind us.

      These days, frameworks insulate developers from lower-level operations and it almost feels alien for a framework fanatic not to be able to translate his/her database knowledge into an object relational mapper.

      For the purpose of not leaving such developers heartbroken and wondering about their existence and purpose in the universe, we are going to look at how to go about the business of JSON columns in the Laravel framework.

      We will only be focusing on the parts that overlap with our subject matter which deals with JSON columns. An in-depth tutorial on the Laravel framework is beyond the scope of this piece.

      Creating the Migrations

      Make sure to configure your Laravel application to use a MySQL database.

      We are going to create three migrations for brands, categories, and products respectively.

      $ php artisan make:migration create_brands
      $ php artisan make:migration create_categories
      $ php artisan make:migration create_products
      

      The create_brands and create_categories migrations are pretty similar and and a regulation for Laravel developers.

      /* database/migrations/create_brands.php */
      
      <?php
      
      use IlluminateSupportFacadesSchema;
      use IlluminateDatabaseSchemaBlueprint;
      use IlluminateDatabaseMigrationsMigration;
      
      class CreateBrands extends Migration
      {
          /**
           * Run the migrations.
           *
           * @return void
           */
          public function up()
          {
              Schema::create('brands', function(Blueprint $table){
                  $table->engine="InnoDB";
                  $table->increments('id');
                  $table->string('name');
                  $table->timestamps();
              });
          }
      
          /**
           * Reverse the migrations.
           *
           * @return void
           */
          public function down()
          {
              Schema::drop('brands');
          }
      }
      
      /* database/migrations/create_categories.php */
      
      <?php
      
      use IlluminateSupportFacadesSchema;
      use IlluminateDatabaseSchemaBlueprint;
      use IlluminateDatabaseMigrationsMigration;
      
      class CreateCategories extends Migration
      {
          /**
           * Run the migrations.
           *
           * @return void
           */
          public function up()
          {
              Schema::create('categories', function(Blueprint $table){
                  $table->engine="InnoDB";
                  $table->increments('id');
                  $table->string('name');
                  $table->timestamps();
              });
          }
      
          /**
           * Reverse the migrations.
           *
           * @return void
           */
          public function down()
          {
              Schema::drop('categories');
          }
      }
      
      

      The create_products migration will also have the directives for indexes and foreign keys.

      /* database/migrations/create_products */
      
      <?php
      
      use IlluminateSupportFacadesSchema;
      use IlluminateDatabaseSchemaBlueprint;
      use IlluminateDatabaseMigrationsMigration;
      
      class CreateProducts extends Migration
      {
          /**
           * Run the migrations.
           *
           * @return void
           */
          public function up()
          {
              Schema::create('products', function(Blueprint $table){
                  $table->engine="InnoDB";
                  $table->increments('id');
                  $table->string('name');
                  $table->unsignedInteger('brand_id');
                  $table->unsignedInteger('category_id');
                  $table->json('attributes');
                  $table->timestamps();
                  // foreign key constraints
                  $table->foreign('brand_id')->references('id')->on('brands')->onDelete('restrict')->onUpdate('cascade');
                  $table->foreign('category_id')->references('id')->on('categories')->onDelete('restrict')->onUpdate('cascade');
                  // indexes
                  $table->index('brand_id');
                  $table->index('category_id');
              });
          }
      
          /**
           * Reverse the migrations.
           *
           * @return void
           */
          public function down()
          {
              Schema::drop('products');
          }
      }
      

      Pay attention to the $table->json('attributes'); statement in the migration.

      Just like creating any other table field using the appropriate data type named method, we have created a JSON column using the json method with the name attributes.

      Also, this only works for database engines that support the JSON data type.

      Engines, such as older versions of MySQL will not be able to carry out these migrations.

      Creating the Models

      Other than associations, there is not much needed to set up our models so let’s run through them quickly.

      /* app/Brand.php */
      
      <?php
      
      namespace App;
      
      use IlluminateDatabaseEloquentModel;
      
      class Brand extends Model
      {
          // A brand has many products
          public function products(){
              return $this->hasMany('Product')
          }
      }
      
      /* app/Category.php */
      
      <?php
      
      namespace App;
      
      use IlluminateDatabaseEloquentModel;
      
      class Category extends Model
      {
          // A category has many products
          public function products(){
              return $this->hasMany('Product')
          }
      }
      
      /* app/Product.php */
      
      <?php
      
      namespace App;
      
      use IlluminateDatabaseEloquentModel;
      
      class Product extends Model
      {
          // Cast attributes JSON to array
          protected $casts = [
              'attributes' => 'array'
          ];
      
          // Each product has a brand
          public function brand(){
              return $this->belongsTo('Brand');
          }
      
          // Each product has a category
          public function category(){
              return $this->belongsTo('Category');
          }
      }
      

      Again, our Product model needs a special mention.

      The $casts array which has the key attributes set to array makes sure whenever a product is fetched from the database, it’s attributes JSON is converted to an associated array.

      We will see later in the tutorial how this facilitates us to update records from our controller actions.

      Resource Operations

      Creating a Product

      Speaking of the admin panel, the parameters to create a product maybe coming in through different routes since we have a number of product categories. You may also have different views to create, edit, show, and delete a product.

      For example, a form to add a camera requires different input fields than a form to add a mobilephone so they warrant separate views.

      Moreover, once you have the user input data, you will most probabaly run it through a request validator, separate for the camera, and the mobilephone each.

      The final step would be to create the product through Eloquent.

      We will be focusing on the camera resource for the rest of this tutorial. Other products can be addressed using the code produced in a similar manner.

      Assuming we are saving a camera and the form fields are named as the respective camera attributes, here is the controller action.

      // creates product in database
      // using form fields
      public function store(Request $request){
          // create object and set properties
          $camera = new AppProduct();
          $camera->name = $request->name;
          $camera->brand_id = $request->brand_id;
          $camera->category_id = $request->category_id;
          $camera->attributes = json_encode([
              'processor' => $request->processor,
              'sensor_type' => $request->sensor_type,
              'monitor_type' => $request->monitor_type,
              'scanning_system' => $request->scanning_system,
          ]);
          // save to database
          $camera->save();
          // show the created camera
          return view('product.camera.show', ['camera' => $camera]);
      }
      

      Fetching Products

      Recall the $casts array we declared earlier in the Product model. It will help us read and edit a product by treating attributes as an associative array.

      // fetches a single product
      // from database
      public function show($id){
          $camera = AppProduct::find($id);
          return view('product.camera.show', ['camera' => $camera]);
      }
      

      Your view would use the $camera variable in the following manner.

      <table>
          <tr>
              <td>Name</td>
              <td>{{ $camera->name }}</td>
          </tr>
          <tr>
              <td>Brand ID</td>
              <td>{{ $camera->brand_id }}</td>
          </tr>
          <tr>
              <td>Category ID</td>
              <td>{{ $camera->category_id }}</td>
          </tr>
          <tr>
              <td>Processor</td>
              <td>{{ $camera->attributes['processor'] }}</td>
          </tr>
          <tr>
              <td>Sensor Type</td>
              <td>{{ $camera->attributes['sensor_type'] }}</td>
          </tr>
          <tr>
              <td>Monitor Type</td>
              <td>{{ $camera->attributes['monitor_type'] }}</td>
          </tr>
          <tr>
              <td>Scanning System</td>
              <td>{{ $camera->attributes['scanning_system'] }}</td>
          </tr>
      </table>
      

      Editing a Product

      As shown in the previous section, you can easily fetch a product and pass it to the view, which in this case would be the edit view.

      You can use the product variable to pre-populate form fields on the edit page.

      Updating the product based on the user input will be pretty similar to the store action we saw earlier, only that instead of creating a new product, you will fetch it first from the database before updating it.

      Searching Based on JSON Attributes

      The last piece of the puzzle that remains to discuss is querying JSON columns using the Eloquent ORM.

      If you have a search page that allows cameras to be searched based on their specifications provided by the user, you can do so with the following code.

      // searches cameras by user provided specifications
      public function search(Request $request){
          $cameras = AppProduct::where([
              ['attributes->processor', 'like', $request->processor],
              ['attributes->sensor_type', 'like', $request->sensor_type],
              ['attributes->monitor_type', 'like', $request->monitor_type],
              ['attributes->scanning_system', 'like', $request->scanning_system]
          ])->get();
          return view('product.camera.search', ['cameras' => $cameras]);
      }
      

      The retrived records will now be available to the product.camera.search view as a $cameras collection.

      Deleting a Product

      Using a non-JSON column attribute, you can delete products by specifying a where clause and then calling the delete method.

      For example, in case of an ID.

      AppProduct::where('id', $id)->delete();
      

      For JSON columns, specify a where clause using a single or multiple attributes and then call the delete method.

      // deletes all cameras with the sensor_type attribute as CMOS
      AppProduct::where('attributes->sensor_type', 'CMOS')->delete();
      }
      

      Curtains

      We have barely scratched the surface when it comes to using JSON columns in MySQL.

      Whenever you need to save data as key/value pairs in a separate table or work with flexible attributes for an entity, you should consider using a JSON data type field instead as it can heavily contribute to compressing your database design.

      If you are interested in diving deeper, the MySQL documentation is a great resource to explore JSON concepts futher.

      I hope you found this tutorial interesting and knowledgeable. Until my next piece, happy coding!



      Source link

      Build a RESTful JSON API With Rails 5 – Part One


      Rails is popularly known for building web applications. Chances are if you’re reading this you’ve built a traditional server-rendered web application with Rails before. If not, I’d highly recommend going through the Getting Started with Rails page to familiarize yourself with the Rails framework before proceeding with this tutorial.

      As of version 5, Rails core now supports API only applications! In previous versions, we relied on an external gem: rails-api which has since been merged to core rails.

      API only applications are slimmed down compared to traditional Rails web applications. According to Rails 5 release notes, generating an API only application will:

      • Start the application with a limited set of middleware
      • Make the ApplicationController inherit from ActionController::API instead of ActionController::Base
      • Skip generation of view files

      This works to generate an API-centric framework excluding functionality that would otherwise be unused and unnecessary.

      In this three-part tutorial, we’ll build a todo list API where users can manage their to-do lists and todo items.

      Prerequisites

      Before we begin, make sure you have ruby version >=2.2.2 and rails version 5.

      $ ruby -v # ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin16]
      $ rails -v # Rails 5.0.1
      

      If your ruby version is not up to date, you can update it with a ruby version manager like rvm or rbenv.

      # when using rbenv
      $ rbenv install 2.3.1
      # set 2.3.1 as the global version
      $ rbenv global 2.3.1
      
      # when using rvm
      $ rvm install 2.3.1
      # set 2.3.1 as the global version
      $ rvm use 2.3.1
      

      If your rails version is not up to date, update to the latest version by running:

      $ gem update rails
      

      All good? Let’s get started!

      API Endpoints

      Our API will expose the following RESTful endpoints.

      EndpointFunctionality
      POST /signupSignup
      POST /auth/loginLogin
      GET /auth/logoutLogout
      GET /todosList all todos
      POST /todosCreate a new todo
      GET /todos/:idGet a todo
      PUT /todos/:idUpdate a todo
      DELETE /todos/:idDelete a todo and its items
      GET /todos/:id/itemsGet a todo item
      PUT /todos/:id/itemsUpdate a todo item
      DELETE /todos/:id/itemsDelete a todo item

      Part One will Cover:

      • Project setup
      • Todos API
      • TodoItems API

      Project Setup

      Generate a new project todos-api by running:

      $ rails new todos-api --api -T
      

      Note that we’re using the --api argument to tell Rails that we want an API application and -T to exclude Minitest the default
      testing framework. Don’t freak out, we’re going to write tests. We’ll be using RSpec instead to test our API. I find RSpec to be more expressive
      and easier to start with as compared to Minitest.

      Dependencies

      Let’s take a moment to review the gems that we’ll be using.

      • rspec-rails – Testing framework.
      • factorybotrails – A fixtures replacement with a more straightforward syntax. You’ll see.
      • shoulda_matchers – Provides RSpec with additional matchers.
      • database_cleaner – You guessed it! It literally cleans our test database to ensure
        a clean state in each test suite.
      • faker – A library for generating fake data. We’ll use this to generate test data.

      All good? Great! Let’s set them up. In your Gemfile:

      Add rspec-rails to both the :development and :test groups.

      # Gemfile
      group :development, :test do
        gem 'rspec-rails', '~> 3.5'
      end
      

      This is a handy shorthand to include a gem in multiple environments.

      Add factory_bot_rails, shoulda_matchers, faker and database_cleaner to the :test group.

      # Gemfile
      group :test do
        gem 'factory_bot_rails', '~> 4.0'
        gem 'shoulda-matchers', '~> 3.1'
        gem 'faker'
        gem 'database_cleaner'
      end
      

      Install the gems by running:

      $ bundle install
      

      Initialize the spec directory (where our tests will reside).

      $ rails generate rspec:install
      

      This adds the following files which are used for configuration:

      • .rspec
      • spec/spec_helper.rb
      • spec/rails_helper.rb

      Create a factories directory (factory bot uses this as the default directory). This is where we’ll define the model factories.

      $ mkdir spec/factories
      

      Configuration

      In spec/rails_helper.rb

      # require database cleaner at the top level
      require 'database_cleaner'
      
      # [...]
      # configure shoulda matchers to use rspec as the test framework and full matcher libraries for rails
      Shoulda::Matchers.configure do |config|
        config.integrate do |with|
          with.test_framework :rspec
          with.library :rails
        end
      end
      
      # [...]
      RSpec.configure do |config|
        # [...]
        # add `FactoryBot` methods
        config.include FactoryBot::Syntax::Methods
      
        # start by truncating all the tables but then use the faster transaction strategy the rest of the time.
        config.before(:suite) do
          DatabaseCleaner.clean_with(:truncation)
          DatabaseCleaner.strategy = :transaction
        end
      
        # start the transaction strategy as examples are run
        config.around(:each) do |example|
          DatabaseCleaner.cleaning do
            example.run
          end
        end
        # [...]
      end
      

      Phew! That was a rather long. Good thing is, it’s a smooth ride from here on out.


      Models

      Let’s start by generating the Todo model

      $ rails g model Todo title:string created_by:string
      

      Notice that we’ve included the model attributes in the model generation command. This way we don’t have to edit the migration file.
      The generator invokes active record and rspec to generate the migration, model, and spec respectively.

      # db/migrate/[timestamp]_create_todos.rb
      class CreateTodos < ActiveRecord::Migration[5.0]
        def change
          create_table :todos do |t|
            t.string :title
            t.string :created_by
      
            t.timestamps
          end
        end
      end
      

      And now the Item model

      $ rails g model Item name:string done:boolean todo:references
      

      By adding todo:references we’re telling the generator to set up an association with the Todo model.
      This will do the following:

      • Add a foreign key column todo_id to the items table
      • Setup a belongs_to association in the Item model
      # db/migrate/[timestamp]_create_items.rb
      class CreateItems < ActiveRecord::Migration[5.0]
        def change
          create_table :items do |t|
            t.string :name
            t.boolean :done
            t.references :todo, foreign_key: true
      
            t.timestamps
          end
        end
      end
      

      Looks good? Let’s run the migrations.

      $ rails db:migrate
      

      We’re Test Driven, let’s write the model specs first.

      # spec/models/todo_spec.rb
      require 'rails_helper'
      
      # Test suite for the Todo model
      RSpec.describe Todo, type: :model do
        # Association test
        # ensure Todo model has a 1:m relationship with the Item model
        it { should have_many(:items).dependent(:destroy) }
        # Validation tests
        # ensure columns title and created_by are present before saving
        it { should validate_presence_of(:title) }
        it { should validate_presence_of(:created_by) }
      end
      

      RSpec has a very expressive DSL (Domain Specific Language). You can almost read the tests like a paragraph.
      Remember our shoulda matchers gem? It provides RSpec with the nifty association and validation matchers above.

      # spec/models/item_spec.rb
      require 'rails_helper'
      
      # Test suite for the Item model
      RSpec.describe Item, type: :model do
        # Association test
        # ensure an item record belongs to a single todo record
        it { should belong_to(:todo) }
        # Validation test
        # ensure column name is present before saving
        it { should validate_presence_of(:name) }
      end
      

      Let’s execute the specs by running:

      $ bundle exec rspec
      

      And to no surprise, we have only one test passing and four failures. Let’s go ahead and fix the failures.

      # app/models/todo.rb
      class Todo < ApplicationRecord
        # model association
        has_many :items, dependent: :destroy
      
        # validations
        validates_presence_of :title, :created_by
      end
      
      # app/models/item.rb
      class Item < ApplicationRecord
        # model association
        belongs_to :todo
      
        # validation
        validates_presence_of :name
      end
      

      At this point run the tests again and…

      voila! All green.


      Controllers

      Now that our models are all setup, let’s generate the controllers.

      $ rails g controller Todos
      $ rails g controller Items
      

      You guessed it! Tests first… with a slight twist. Generating controllers by default generates controller specs.
      However, we won’t be writing any controller specs. We’re going to write request specs instead.

      Request specs are designed to drive behavior through the full stack, including routing. This means they can hit the applications’
      HTTP endpoints as opposed to controller specs which call methods directly. Since we’re building an API application, this is exactly the kind of behavior we want from our tests.

      According to RSpec, the official recommendation of the Rails team and the RSpec core team is to write request specs instead.

      Add a requests folder to the spec directory with the corresponding spec files.

      $ mkdir spec/requests && touch spec/requests/{todos_spec.rb,items_spec.rb} 
      

      Before we define the request specs, Let’s add the model factories which will provide the test data.

      Add the factory files:

      $ touch spec/factories/{todos.rb,items.rb}
      

      Define the factories.

      # spec/factories/todos.rb
      FactoryBot.define do
        factory :todo do
          title { Faker::Lorem.word }
          created_by { Faker::Number.number(10) }
        end
      end
      

      By wrapping faker methods in a block, we ensure that faker generates dynamic data every time the factory is invoked.
      This way, we always have unique data.

      # spec/factories/items.rb
      FactoryBot.define do
        factory :item do
          name { Faker::StarWars.character }
          done false
          todo_id nil
        end
      end
      

      Todo API

      # spec/requests/todos_spec.rb
      require 'rails_helper'
      
      RSpec.describe 'Todos API', type: :request do
        # initialize test data 
        let!(:todos) { create_list(:todo, 10) }
        let(:todo_id) { todos.first.id }
      
        # Test suite for GET /todos
        describe 'GET /todos' do
          # make HTTP get request before each example
          before { get '/todos' }
      
          it 'returns todos' do
            # Note `json` is a custom helper to parse JSON responses
            expect(json).not_to be_empty
            expect(json.size).to eq(10)
          end
      
          it 'returns status code 200' do
            expect(response).to have_http_status(200)
          end
        end
      
        # Test suite for GET /todos/:id
        describe 'GET /todos/:id' do
          before { get "/todos/#{todo_id}" }
      
          context 'when the record exists' do
            it 'returns the todo' do
              expect(json).not_to be_empty
              expect(json['id']).to eq(todo_id)
            end
      
            it 'returns status code 200' do
              expect(response).to have_http_status(200)
            end
          end
      
          context 'when the record does not exist' do
            let(:todo_id) { 100 }
      
            it 'returns status code 404' do
              expect(response).to have_http_status(404)
            end
      
            it 'returns a not found message' do
              expect(response.body).to match(/Couldn't find Todo/)
            end
          end
        end
      
        # Test suite for POST /todos
        describe 'POST /todos' do
          # valid payload
          let(:valid_attributes) { { title: 'Learn Elm', created_by: '1' } }
      
          context 'when the request is valid' do
            before { post '/todos', params: valid_attributes }
      
            it 'creates a todo' do
              expect(json['title']).to eq('Learn Elm')
            end
      
            it 'returns status code 201' do
              expect(response).to have_http_status(201)
            end
          end
      
          context 'when the request is invalid' do
            before { post '/todos', params: { title: 'Foobar' } }
      
            it 'returns status code 422' do
              expect(response).to have_http_status(422)
            end
      
            it 'returns a validation failure message' do
              expect(response.body)
                .to match(/Validation failed: Created by can't be blank/)
            end
          end
        end
      
        # Test suite for PUT /todos/:id
        describe 'PUT /todos/:id' do
          let(:valid_attributes) { { title: 'Shopping' } }
      
          context 'when the record exists' do
            before { put "/todos/#{todo_id}", params: valid_attributes }
      
            it 'updates the record' do
              expect(response.body).to be_empty
            end
      
            it 'returns status code 204' do
              expect(response).to have_http_status(204)
            end
          end
        end
      
        # Test suite for DELETE /todos/:id
        describe 'DELETE /todos/:id' do
          before { delete "/todos/#{todo_id}" }
      
          it 'returns status code 204' do
            expect(response).to have_http_status(204)
          end
        end
      end
      

      We start by populating the database with a list of 10 todo records (thanks to factory bot).
      We also have a custom helper method json which parses the JSON response to a Ruby Hash which is easier to work with in our tests.
      Let’s define it in spec/support/request_spec_helper.

      Add the directory and file:

      $ mkdir spec/support && touch spec/support/request_spec_helper.rb
      
      # spec/support/request_spec_helper
      module RequestSpecHelper
        # Parse JSON response to ruby hash
        def json
          JSON.parse(response.body)
        end
      end
      

      The support directory is not autoloaded by default. To enable this, open the rails helper and comment out the support directory auto-loading and then
      include it as shared module for all request specs in the RSpec configuration block.

      # spec/rails_helper.rb
      # [...]
      Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
      # [...]
      RSpec.configuration do |config|
        # [...]
        config.include RequestSpecHelper, type: :request
        # [...]
      end
      

      Run the tests.

      We get failing routing errors. This is because we haven’t defined the routes yet. Go ahead and define them in config/routes.rb.

      # config/routes.rb
      Rails.application.routes.draw do
        resources :todos do
          resources :items
        end
      end
      

      In our route definition, we’re creating todo resource with a nested items resource. This enforces the 1:m (one to many) associations at the routing level.
      To view the routes, you can run:

      $ rails routes
      

      When we run the tests we see that the routing error is gone. As expected we have controller failures. Let’s go ahead and define the controller methods.

      # app/controllers/todos_controller.rb
      class TodosController < ApplicationController
        before_action :set_todo, only: [:show, :update, :destroy]
      
        # GET /todos
        def index
          @todos = Todo.all
          json_response(@todos)
        end
      
        # POST /todos
        def create
          @todo = Todo.create!(todo_params)
          json_response(@todo, :created)
        end
      
        # GET /todos/:id
        def show
          json_response(@todo)
        end
      
        # PUT /todos/:id
        def update
          @todo.update(todo_params)
          head :no_content
        end
      
        # DELETE /todos/:id
        def destroy
          @todo.destroy
          head :no_content
        end
      
        private
      
        def todo_params
          # whitelist params
          params.permit(:title, :created_by)
        end
      
        def set_todo
          @todo = Todo.find(params[:id])
        end
      end
      

      More helpers. Yay! This time we have:

      • json_response which does… yes, responds with JSON and an HTTP status code (200 by default).
        We can define this method in concerns folder.
      # app/controllers/concerns/response.rb
      module Response
        def json_response(object, status = :ok)
          render json: object, status: status
        end
      end
      
      • set_todo – callback method to find a todo by id. In the case where the record does not exist, ActiveRecord
        will throw an exception ActiveRecord::RecordNotFound. We’ll rescue from this exception and return a 404 message.
      # app/controllers/concerns/exception_handler.rb
      module ExceptionHandler
        # provides the more graceful `included` method
        extend ActiveSupport::Concern
      
        included do
          rescue_from ActiveRecord::RecordNotFound do |e|
            json_response({ message: e.message }, :not_found)
          end
      
          rescue_from ActiveRecord::RecordInvalid do |e|
            json_response({ message: e.message }, :unprocessable_entity)
          end
        end
      end
      

      In our create method in the TodosController, note that we’re using create! instead of create. This way, the model will raise
      an exception ActiveRecord::RecordInvalid. This way, we can avoid deep nested if statements in the controller. Thus, we rescue from this exception
      in the ExceptionHandler module.

      However, our controller classes don’t know about these helpers yet. Let’s fix that by including these modules in the
      application controller.

      # app/controllers/application_controller.rb
      class ApplicationController < ActionController::API
        include Response
        include ExceptionHandler
      end
      

      Run the tests and everything’s all green!

      Let’s fire up the server for some good old manual testing.

      $ rails s
      

      Now let’s go ahead and make requests to the API. I’ll be using httpie as my HTTP client.

      # GET /todos
      $ http :3000/todos
      # POST /todos
      $ http POST :3000/todos title=Mozart created_by=1
      # PUT /todos/:id
      $ http PUT :3000/todos/1 title=Beethoven
      # DELETE /todos/:id
      $ http DELETE :3000/todos/1
      

      You should see similar output.


      TodoItems API

      # spec/requests/items_spec.rb
      require 'rails_helper'
      
      RSpec.describe 'Items API' do
        # Initialize the test data
        let!(:todo) { create(:todo) }
        let!(:items) { create_list(:item, 20, todo_id: todo.id) }
        let(:todo_id) { todo.id }
        let(:id) { items.first.id }
      
        # Test suite for GET /todos/:todo_id/items
        describe 'GET /todos/:todo_id/items' do
          before { get "/todos/#{todo_id}/items" }
      
          context 'when todo exists' do
            it 'returns status code 200' do
              expect(response).to have_http_status(200)
            end
      
            it 'returns all todo items' do
              expect(json.size).to eq(20)
            end
          end
      
          context 'when todo does not exist' do
            let(:todo_id) { 0 }
      
            it 'returns status code 404' do
              expect(response).to have_http_status(404)
            end
      
            it 'returns a not found message' do
              expect(response.body).to match(/Couldn't find Todo/)
            end
          end
        end
      
        # Test suite for GET /todos/:todo_id/items/:id
        describe 'GET /todos/:todo_id/items/:id' do
          before { get "/todos/#{todo_id}/items/#{id}" }
      
          context 'when todo item exists' do
            it 'returns status code 200' do
              expect(response).to have_http_status(200)
            end
      
            it 'returns the item' do
              expect(json['id']).to eq(id)
            end
          end
      
          context 'when todo item does not exist' do
            let(:id) { 0 }
      
            it 'returns status code 404' do
              expect(response).to have_http_status(404)
            end
      
            it 'returns a not found message' do
              expect(response.body).to match(/Couldn't find Item/)
            end
          end
        end
      
        # Test suite for PUT /todos/:todo_id/items
        describe 'POST /todos/:todo_id/items' do
          let(:valid_attributes) { { name: 'Visit Narnia', done: false } }
      
          context 'when request attributes are valid' do
            before { post "/todos/#{todo_id}/items", params: valid_attributes }
      
            it 'returns status code 201' do
              expect(response).to have_http_status(201)
            end
          end
      
          context 'when an invalid request' do
            before { post "/todos/#{todo_id}/items", params: {} }
      
            it 'returns status code 422' do
              expect(response).to have_http_status(422)
            end
      
            it 'returns a failure message' do
              expect(response.body).to match(/Validation failed: Name can't be blank/)
            end
          end
        end
      
        # Test suite for PUT /todos/:todo_id/items/:id
        describe 'PUT /todos/:todo_id/items/:id' do
          let(:valid_attributes) { { name: 'Mozart' } }
      
          before { put "/todos/#{todo_id}/items/#{id}", params: valid_attributes }
      
          context 'when item exists' do
            it 'returns status code 204' do
              expect(response).to have_http_status(204)
            end
      
            it 'updates the item' do
              updated_item = Item.find(id)
              expect(updated_item.name).to match(/Mozart/)
            end
          end
      
          context 'when the item does not exist' do
            let(:id) { 0 }
      
            it 'returns status code 404' do
              expect(response).to have_http_status(404)
            end
      
            it 'returns a not found message' do
              expect(response.body).to match(/Couldn't find Item/)
            end
          end
        end
      
        # Test suite for DELETE /todos/:id
        describe 'DELETE /todos/:id' do
          before { delete "/todos/#{todo_id}/items/#{id}" }
      
          it 'returns status code 204' do
            expect(response).to have_http_status(204)
          end
        end
      end
      

      As expected, running the tests at this point should output failing todo item tests. Let’s define the todo items controller.

      # app/controllers/items_controller.rb
      class ItemsController < ApplicationController
        before_action :set_todo
        before_action :set_todo_item, only: [:show, :update, :destroy]
      
        # GET /todos/:todo_id/items
        def index
          json_response(@todo.items)
        end
      
        # GET /todos/:todo_id/items/:id
        def show
          json_response(@item)
        end
      
        # POST /todos/:todo_id/items
        def create
          @todo.items.create!(item_params)
          json_response(@todo, :created)
        end
      
        # PUT /todos/:todo_id/items/:id
        def update
          @item.update(item_params)
          head :no_content
        end
      
        # DELETE /todos/:todo_id/items/:id
        def destroy
          @item.destroy
          head :no_content
        end
      
        private
      
        def item_params
          params.permit(:name, :done)
        end
      
        def set_todo
          @todo = Todo.find(params[:todo_id])
        end
      
        def set_todo_item
          @item = @todo.items.find_by!(id: params[:id]) if @todo
        end
      end
      

      Run the tests.

      Run some manual tests for the todo items API:

      # GET /todos/:todo_id/items
      $ http :3000/todos/2/items
      # POST /todos/:todo_id/items
      $ http POST :3000/todos/2/items name="Listen to 5th Symphony" done=false
      # PUT /todos/:todo_id/items/:id
      $ http PUT :3000/todos/2/items/1 done=true
      # DELETE /todos/:todo_id/items/1
      $ http DELETE :3000/todos/2/items/1
      


      Conclusion

      That’s it for part one! At this point you should have learned how to:

      • Generate an API application with Rails 5
      • Setup RSpec testing framework with Factory Bot, Database Cleaner, Shoulda Matchers and Faker.
      • Build models and controllers with TDD (Test Driven Development).
      • Make HTTP requests to an API with httpie.

      In the next part, we’ll cover authentication with JWT, pagination, and API versioning. Hope to see you there. Cheers!



      Source link