Filippos Vasilakis
Web dev
You build the API for the client; not for yourself!
Resource
+
Collection
Collection
Design
}
}
-
What is a controller in Rails?
What is a controller in Rails?
Controllers are just Rack wrappers
This is the request-response cycle
(and should be really really fast)
Given that I have authenticated the user in the request, I need to know 3 things:
Tips:
Resource
+
Collection
Collection
Design
}
}
-
GET /api/v1/videos?fields[title]=true&fields[description]=true&fields[links]=true
(JSONAPI: GET /api/v1/videos?fields=[title,description,links])
{
"video": {
"id": 3031,
"title": "Leon på havet!",
"description": " #bohuslän",
"links":{
"user": "/api/v1/users/18"
}
}
}
2. Granular Permissions
(as a guest): GET /api/v1/user/17
{
"user": {
"id": 17,
"username": "robert",
"name": "Robert M",
"links": {
"videos":"/api/v1/users/17/videos"
}
}
}
(as a regular user): GET /api/v1/user/17
{
"user": {
"id": 17,
"username": "robert",
"name": "Robert M",
"description": "My videos! Just small stories.",
"avatar": "4c657cfe-116d-4f3f-694d-9164e5294e72.jpg",
"background": "a6648b87-c64e-d0b9-a75e-18b635a2a89e",
"url": "http://www.example.com",
"followers_count": 10,
"followings_count": 16,
"videos_count": 405,
"links": {
"videos":"/api/v1/users/17/videos"
}
}
}
Example: GET /api/v1/videos?fields[title]&includes=[user]&fields[user][username]=true
(JSONAPI: GET /api/v1/videos?fields=title,user.username&includes=user)
{
"video": {
"id": 3031,
"title": "Leon på havet!",
"user": {
"id": 18,
"username": "robert",
"links": {
"videos":"/api/v1/users/17/videos"
}
}
"links":{
"user": "/api/v1/users/17"
}
}
}
Example: GET /api/v1/videos?sort[username]=desc&per_page=5&page=3&offset=2
(JSONAPI: GET /api/v1/videos?sort=-username&per_page=5&page=3&offset=2)
Pagination should support:
Example: GET /api/v1/videos?draft=true
(JSONAPI: GET /api/v1/videos?field[draft]=true)
Example: GET /api/v1/comments?post[group_id]=1 //ask comments that belong to a post that belongs to group_id=1
(JSONAPI: GET /api/v1/comments?post[group_id]=1)
Example: GET /api/v1/videos?draft=true&published=true
(JSONAPI: GET /api/v1/videos?field[draft,published]=true)
Example: GET /api/v1/videos?state='published'&aggregate[view_count][sum]=true&aggregate[view_count&[avg]=true&per_page=1
this.store.query('video', {
published: true,
aggregate: {
view_count: {
sum: true,
avg: true
}
},
per_page:1 //optimization!
});
The URL might seem a bit complicated but params is the easiest thing to create in a URL using any language.
In JS using ember-data:
A video:
GET /api/v1/videos/:video_id
A post:
GET /api/v1/posts/:post_id
A group:
GET /api/v1/groups/:group_id
A Comment:
GET /api/v1/comments/:comment_id
Let's add comments to a video
GET /api/v1/videos/:video_id/comments
Let's add comments to a post:
GET /api/v1/post/:post_id/comments
Let's add comment to a.. comment! (of a video!)
GET /api/v1/videos/:video_id/comments/:comment_id/comments
Doesn't look promising!
Flat design
(unless you follow a spec like JSONAPI, Sirien, Hal which would be great!)
Let's ask for comments of a post that belongs to a group:
GET /api/v1/videos/:group_id/posts/:post_id/comments
Let's ask for comments of a comment of a post that belongs to a group:
GET /api/v1/videos/:group_id/posts/:post_id/comments/:comment_id/comments
You see where is going..... and this is only for 1 resource! For 4 resources
it's going to be hell!
Why not just:
GET /api/v1/comments?video_id=1
GET /api/v1/comments?post_id=1
GET /api/v1/comments?video_id=1&comment_id=3
GET /api/v1/comments?group_id=1&comment_id=3
GET /api/v1/comments?group_id=1&post_id=3&comment_id=3
POST /api/v1/comments //all comment association data are in the body of the POST request
Of course some resources can't be flat: followers
(followers of what/whom?!)
GET /api/v1/comments?post_id[]=1&video_id[]=3
GET /api/v1/comments?user_id[]=1&user_id[]=3
Nested urls: You can't request a resource for multiple parent resources.
For instance, let's say that I want all comments of 2 videos/posts etc.
That's impossible with the nesting pattern. Instead:
Remember that constructing params of a url in the client is much much easier
than constructing the actual URL. After all you want comments, that is, the
resource is the same. Why not having a conistent url for that?
Yes
Resource
+
Collection
Collection
Design
}
}
-
module AuthorizeWithReturn
def authorize_with_permissions(record, query=nil)
query ||= params[:action].to_s + '?'
@_pundit_policy_authorized = true
policy = policy(record)
policy.public_send(query)
end
def included
super
hide_action :authorize
end
end
module Pundit
prepend AuthorizeWithReturn
end
(with a small monkey patch)
It doesn't raise an error but returns what policy object returns
module FlexiblePermissions
module RoleMethods
attr_reader :record, :model
def initialize(record, model = nil)
@record = record
@model = model || record.class
end
def fields(asked = nil)
self.class::Fields.new(asked, record, model).resolve
end
def includes(asked = nil)
self.class::Includes.new(asked, record, model).resolve
end
def collection
record
end
end
end
module FlexiblePermissions
module SparsedMethods
attr_reader :resolve, :model, :record, :asked
def initialize(asked, record, model)
@model = model
@asked = asked
@record = record
end
def resolve
return defaults if asked.blank?
union(permitted, asked)
end
def permitted
[]
end
def defaults
permitted
end
def union(permitted, asked = nil)
return permitted unless asked.is_a?(Array)
permitted.map(&:to_sym) & asked.map(&:to_sym)
end
def collection?
record.kind_of? ActiveRecord::Relation
end
end
end
module FlexiblePermissions
module SparsedFieldMethods
include SparsedMethods
def permitted
model.attribute_names.map(&:to_sym)
end
end
module SparsedIncludeMethods
include SparsedMethods
def permitted
model.reflect_on_all_associations.map(&:name).map(&:to_sym)
end
end
end
class UserPolicy < ApplicationPolicy
class Admin < DefaultPermissions
class Fields < self::Fields
def permitted
super + [
:links, :following_state, :follower_state, :notification_count
]
end
def defaults
super - [:notification_count]
end
end
class Includes < self::Includes
def permitted
[]
end
end
end
end
class ApplicationPolicy
class DefaultPermissions
include FlexiblePermissions::RoleMethods
class Fields
include FlexiblePermissions::SparsedFieldMethods
end
class Includes
include FlexiblePermissions::SparsedIncludeMethods
end
end
end
Admin is allowed all model fields + extra computed properties
class UserPolicy < ApplicationPolicy
class Owner < Admin
class Fields < self::Fields
def permitted
super - [:updated_at, :suspended_at, :reference_id]
end
def defaults
if collection?
super - [
:user_id, :background, :date_of_birth, :description,
:email_confirmed, :followers_count, :followings_count, :gender,
:mobile, :private, :suspended_at, :terms_accepted, :url,
:videos_count, :email, :country_code
]
else
permitted - [:notification_count, :preferences]
end
end
end
class Includes < self::Includes
def permitted
[]
end
end
end
class Regular < Owner
class Fields < self::Fields
def permitted
super - [:notification_count, :preferences, :email, :email_confirmed,
:mobile, :suspended_at, :terms_accepted, :role, :created_at]
end
end
end
end
class UserPolicy < ApplicationPolicy
def create?
return Admin.new(record) if user && user.admin?
return Regular.new(record)
end
def show?
raise Pundit::NotAuthorizedError unless user
return Admin.new(record) if user.admin?
return Owner.new(record) if user.id == record.id
return Regular.new(record)
end
def update?
raise Pundit::NotAuthorizedError unless user
return Admin.new(record) if user.admin?
return Owner.new(record) if user.id == record.id
raise Pundit::NotAuthorizedError
end
def destroy?
return Admin.new(record) if user.admin?
return Owner.new(record)
end
end
we don't raise an error but return the actual permissions
(unless you don't have the permissions --> 403)
Embrace inheritance to inherit permissions and defaults!
class Api::V1::UsersController < Api::V1::BaseController
def show
auth_user = authorize_with_permissions(@user, :show?)
render json: auth_user.record, serializer: UserSerializer::Resource,
fields: auth_user.fields(params[:fields]),
include: auth_user.includes(params[:include])
end
end
class UserSerializer < BaseSerializer
def notification_count
...
end
.
.
.
class Collection < self
attributes(*User.attribute_names.map(&:to_sym))
attribute :notification_count
attribute :following_state
attribute :follower_state
attribute :links
end
class Resource < Collection
end
end
Embrace inheritance :)
Resource
+
Collection
Collection
Design
}
}
-
class Api::V1::CommentsController < Api::V1::BaseController
def index
comments = Comment.all
comments = comments.scope_by(params[:scope]) if params[:scope]
[:id, :video_id, :user_id, :post_id, :tags].each do |prm|
comments = comments.where(prm => params[prm]) unless params[prm].blank?
end
unless params[:published_at_start].blank?
comments = comments.where('PUBLISHED_AT >= ?', params[:published_at_start])
end
unless params[:published_at_end].blank?
comments = comments.where('PUBLISHED_AT <= ?', params[:published_at_end])
end
unless params[:text].blank?
pdsas = pdsas.where('TEXT ILIKE ?', "%#{params[:text]}%")
end
comments = policy_scope(comments)
render json: comments, each_serializer: Api::V1::CommentSerializer
end
end
A typical controller index....
Don't re-invent the wheel: active_hash_relation
Runs AR queries from defined in a hash
apply_filters(Video.all, {
created_at: {geq: 2016-01-24}, user_id: 17, views_count: {geq: 1000}, published: true,
tags: ['foo', 'bar'], description: { like: { starts_with: 'tennis' } }, limit: 30,
sort: {property: :published_at, order: :desc}
}
SELECT "videos".* FROM "videos"
WHERE (videos.title IN ('foo', 'bar'))
AND (videos.user_id = 17)
AND (videos.created_at >= 1991)
AND (videos.views_count >= 1000)
AND (videos.tags IN ('foo', 'bar'))
AND (videos.user_id = 17)
AND (videos.created_at >= 1991)
AND (videos.views_count >= 1000)
AND (videos.description LIKE 'tennis%')
ORDER BY "videos"."published_at" DESC LIMIT 30
this.get('store').find('video', {
created_at: {
geq: '2016-01-24'
},
user_id: 17,
views_count: {
geq: 1000
},
published: true,
tags: ['foo', 'bar'],
description: {
like: { starts_with: 'tennis' }
},
limit: 30,
sort: { property: 'published_at', order: 'desc }
});
This is SUPER important for the front-end team. It facilitates them to construct any UI they want without asking you a new API endpoint
class Api::V1::CommentsFilters < Api::V1::BaseFilter
def collection
self.context = apply_filters(context, filter_permitted_params)
paginate(self.context)
end
private
def filter_permitted_params
params[:sort] = {property: :created_at, order: :desc} if params[:sort].blank?
params.permit(:id, :video_id, :text, published_at: [:le, :ge, :leq, :geq],
sort: [:property, :order])
end
end
Let me introduce you to Filter classes
They help controllers to lose weight
There is also mongoid_hash_query for Mongoid
Both support aggregation queries
(min, max, sum, average)
class Api::V1::CommentsController < Api::V1::BaseController
before_action :load_resource
def index
@comments = policy_scope(@comments)
render json: @comments, each_serializer: CommentSerializer::Collection,
meta: meta_attributes(@comments)
end
.
.
.
def load_resource
case params[:action].to_sym
when :index
@comments = Api::V1::CommentsFilters.new(Comment.all, params).collection
when :create
@comment = Comment.new(permitted_params)
@comment.user_id = current_user.id if @comment.user_id.nil?
else
@comment = Comment.find(params[:id])
end
end
end
Super clean controller
note the default user_id there
Resource
+
Collection
Collection
Design
}
}
-
Easy in Rails
Resource
+
Collection
Collection
Design
}
}
-
New API endpoint process:
describe Api::V1::UsersController, type: :api do
context :index do
before do
create_and_sign_in_user
5.times{ FactoryGirl.create(:user) }
get api_v1_users_path, format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(200)
end
it 'returns the correct number of data in the body' do
body = HashWithIndifferentAccess.new(MultiJson.load(last_response.body))
expect(body[:users].length).to eql(5)
end
end
end
describe Api::V1::UsersController, type: :api do
context :create do
before do
create_and_sign_in_user
@user = FactoryGirl.attributes_for(:user)
post api_v1_users_path, county: @user.as_json, format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(201)
end
it 'returns the data in the body' do
user = User.last!
body = HashWithIndifferentAccess.new(MultiJson.load(last_response.body))
expect(body[:user][:name]).to eql(@user.name)
expect(body[:user][:updated_at]).to eql(user.updated_at.iso8601) #@user var has nil updated_at
end
end
end
describe Api::V1::UsersController, type: :api do
context :update do
before do
create_and_sign_in_user
@user = FactoryGirl.create(:user)
@user.name = 'Another name'
put api_v1_user_path(@user.id), county: @user.as_json, format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(200)
end
it 'returns the correct location' do
expect(last_response.headers['Location'])
.to include(api_v1_county_path(@user.id))
end
it 'returns the data in the body' do
user = User.last!
body = HashWithIndifferentAccess.new(MultiJson.load(last_response.body))
expect(body[:user][:name]).to eql(@user.name)
expect(body[:user][:updated_at]).to eql(user.updated_at.iso8601)
end
end
end
describe Api::V1::UsersController, type: :api do
context :destroy do
context 'when the resource does NOT exist' do
before do
create_and_sign_in_user
@user = FactoryGirl.create(:user)
delete api_v1_user_path(rand(100..1000)), format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(404)
end
end
context 'when the resource does exist' do
before do
create_and_sign_in_user
@user = FactoryGirl.create(:user)
delete api_v1_user_path(@user.id), format: :json
end
it 'returns the correct status' do
expect(last_response.status).to eql(204)
end
it 'actually deletes the resource' do
expect(User.find_by(id: @user.id)).to eql(nil)
end
end
end
end
We tested:
Basically we re-implement the RSpecs methods respond_to and rspec-rails' be_valid methods at a higher level.
In 110 lines of code..
What if I change my serializer and use HAL? Oh crap...
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://videofy.me/user#",
"type": "object",
"properties": {
"user": {
"id": "user",
"type": "object",
"properties": {
"id": {
"id": "id",
"type": "integer"
},
"username": {
"id": "username",
"type": "string"
},
"name": {
"id": "name",
"type": "string"
},
"gender": {
"id": "gender",
"type": { "enum": ["male", "female", "null"] }
}
},
"additionalProperties": false,
"required": [
"id",
"username",
"name",
"gender"
]
}
},
"additionalProperties": false,
"required": [
"user"
]
}
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "http://videofy.me/like#",
"type": "object",
"properties": {
"like": {
"id": "like",
"type": "object",
"properties": {
"id": {
"id": "id",
"type": "integer"
},
"video_id": {
"id": "video_id",
"type": "integer"
},
"user": {
"$ref": "http://videofy.me/user#/properties/user",
"additionalProperties": false
}
},
"additionalProperties": false,
"required": [
"video_id",
"user",
"links"
]
}
},
"additionalProperties": false,
"required": [
"like"
]
}
RSpec::Matchers.define :match_response_schema do |expected|
match do |actual|
parse_and_validate(expected, actual)[0]
end
failure_message do |actual|
parse_and_validate(expected, actual)[1].map{|e|
str = "Error in #{e.path.join('/')} #{e.type}: #{e.message}"
if e.sub_errors
e.sub_errors[1].each do |sub_e|
str += "\n"
str += "Description #{sub_e.schema.try(:data).try(:[],"properties").try(:[],"type").try(:[],"id")}\n"
str += "\t\t Error in #{sub_e.path.join('/')} #{sub_e.type}: #{sub_e.message}"
end
end
str
}.join("\n")
end
def parse_and_validate(schema, json)
schema_directory = "#{Rails.root}/spec/schemas"
schema_path = "#{schema_directory}/#{schema.gsub('.json','')}.json"
schema = JsonSchema.parse!(JSON.parse(File.read(schema_path)))
schema.expand_references!(:store => $document_store)
#binding.pry
return schema.validate(
json.is_a?(String)? JSON.parse(json) : json
)
end
end
Using json-schema gem and applying some matchers :)
Using json-schema gem and applying some matchers :)
describe 'Likes API: :create', type: :api do
context 'with guest user permissions' do
before do
video = FactoryGirl.create(:video, state: :published)
post api_v1_video_like_path(video_id: video.id)
end
it_returns_status(401)
end
context 'with regular user permissions' do
context 'when user has already liked' do
before do
user = create_and_sign_in_user
video = FactoryGirl.create(:video, state: 'published')
like = FactoryGirl.create(:like, user: user, video: video)
post api_v1_video_like_path(video_id: like.video.id)
end
it_returns_status(422)
end
context 'with correct params' do
before do
create_and_sign_in_user
video = FactoryGirl.create(:video)
post api_v1_video_like_path(video_id: video.id)
end
it_returns_status(201)
it_follows_json_schema('strict_default/regular/like/show')
end
end
end
We also use rspec-api_helpers to test http status and actual values!
require 'rails_helper'
describe 'Videos API:', type: :api do
context 'with guest user permissions' do
before do
get api_v1_videos_path, format: :json
end
it_returns_status(401)
end
context 'with regular user permissions' do
before do
create_and_sign_in_user
3.times{ FactoryGirl.create(:video, title: nil) } #drafts
@videos = 5.times{ FactoryGirl.create(:video, state: 'published') }
get api_v1_videos_path, format: :json
end
it_returns_status(200)
it_returns_collection_size(resource: 'videos', size: 5)
it_follows_json_schema('strict_default/regular/video/index')
end
end
We also use rspec-api_helpers to test http status and actual values!
Be ready for huge concenssions when it comes to performance
We ditched Kaminari for that and used our own postgres pagination
1. Improve pagination
Embrace infinite scrolling :)
Use workers whenever possible. For instance:
2. Background Jobs and batch updates with caching
Find the most popular/heavy route in your API (/api/v1/videos/:id/like)
Use JSON Schemas to reflect model validations without touching the model
Return success as soon as it's valid and add it to the queue.
Client should update it's own store after a 2xx
Related: HTTP Prefer header (and rack-prefer)
You can take it one step further and batch like updates in Redis before adding them to the queue. Then every 1 hour do a batch update in db in a worker.
Batch your AR update callbacks too
(given that your client implements it correctly)
3. HTTP Caching
class Api::V1::UsersController < Api::V1::BaseController
before_action :load_resource
def index
auth_users = policy_scope(@users)
with_cache auth_users.collection do
render json: auth_users.collection,
each_serializer: UserSerializer::Collection,
fields: auth_users.fields(params[:fields]),
include: auth_users.includes(params[:include]),
meta: meta_attributes(auth_users.collection)
end
end
def show
auth_user = authorize_with_permissions(@user, :show?)
with_cache auth_user.record do
render json: auth_user.record, serializer: UserSerializer::Resource,
fields: auth_user.fields(params[:fields]),
include: auth_user.includes(params[:include])
end
end
end
3. HTTP Caching
module HttpCaching
extend ActiveSupport::Concern
included do
def with_cache(resource, last_modified_at: :updated_at)
return yield if Rails.application.secrets.disable_http_caching
case resource
when ActiveRecord::Relation
return yield if stale?(
versioned_cache_headers(resource.maximum(last_modified_at))
)
when Mongoid::Criteria
return yield if stale?(
versioned_cache_headers(resource.max(last_modified_at))
)
else
return yield if stale?(
versioned_cache_headers(resource.send(last_modified_at))
)
end
end
def versioned_cache_headers(value)
default_caching_options = {public: true, template: false}
{
etag: value.to_s + Rails.application.secrets.etag_version.to_s,
last_modified: value || DateTime.now + Rails.application.secrets.etag_version.to_i
}.merge(default_caching_options)
end
end
end
Remove caching instantly if needed
Invalidate cache instantly if needed
4. (Smart) low level caching
class Following < ActiveRecord::Base
include ActiveCash
include FollowingScopes
caches :existence, find_by: [:user_id, :following_id],
update_on: [], returns: :state, as: :existence_state
.
.
.
end
ActiveCash
class Metafield < ActiveRecord::Base
include IdentityCache
belongs_to :owner, :polymorphic => true
cache_belongs_to :owner
end
class Product < ActiveRecord::Base
include IdentityCache
has_many :metafields, :as => 'owner'
cache_has_many :metafields, :inverse_name => :owner
end
IdentityCache
class Like < ActiveRecord::Base
include ActiveCash
caches :existence, find_by: [:user_id, :video_id]
.
.
.
end
4. (Smart) low level caching
ActiveCash
5. JWT (and oauth2)
hmac_secret = 'my$ecretK3y'
payload = {user_id: 17, name: 'Filippos Vasilakis', username: 'vasilakisfil'}
token = JWT.encode payload, hmac_secret, 'HS256'
# eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ0ZXN0IjoiZGF0YSJ9._sLPAGP-IXgho8BkMGQ86N2mah7vDyn0L5hOR4UkfoI
puts token
decoded_token = JWT.decode token, hmac_secret, true, { :algorithm => 'HS256' }
# Array
# [
# {"user_id"=>17, "name" => "Filippos Vasilakis", "username" => "vasilakisfil"}, # payload
# {"typ"=>"JWT", "alg"=>"HS256"} # header
# ]
6. Duplicate data in Postgres
{
"video": {
"id": 3031,
"title": "Leon på havet!",
"description": " #bohuslän",
"state": "published",
"file": "https://d2tyds7k2z0x7k.cloudfront.net/4bc54b39-a869-42ad-9d2e-d7c0ed49f3d3.mp4",
"thumbnail": "https://d2rgaput4dpbfs.cloudfront.net/f8b7a960-698c-ca05-d913-f77f832bf118_custom.jpg",
"views_count": 52,
"likes_count": 1,
"comments_count": 0,
"created_at": "2015-08-05T15:56:05.000+02:00",
"user_id": 18,
"music_track_id": 13,
"links": {
"self": "/api/v1/videos/3031",
"comments": "/api/v1/videos/3031/comments",
"user": "/api/v1/users/18"
},
"likes": [{
"user_id": 18,
"video_id": 3031,
"user": {
"id": 18,
"username": "robert",
"name": "Robert M",
"avatar": "https://dxxs0s19ll2ye.cloudfront.net/4c657cfe-116d-4f3f-694d-9164e5294e72.jpg"
},
"links":{
"video": "/api/v1/videos/3031",
"user": "/api/v1/users/18"
}
}],
"requester_has_liked": false //cached using ActiveCash
}
}
We know that 99.9% of video requests, some video likes are also needed by the UI
The iOS UI actually needs the avatars of your friends who liked the video
The Web UI needs avatars and usernames/names
This kills the db!
6. Duplicate data in Postgres
create_table "likes", force: :cascade do |t|
t.integer "video_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
A like in the database holds only references
create_table "likes", force: :cascade do |t|
t.integer "video_id", null: false
t.integer "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.jsonb "info", default: {}, null: false
end
irb(main):010:0> Like.first.as_json
{
"id"=>1, "video_id"=>24454, "user_id"=>257902, "created_at"=>Tue, 19 Jan 2016 05:05:58 UTC +00:00,
"updated_at"=>Tue, 19 Jan 2016 05:05:58 UTC +00:00}, "info" => {
"user" => {"username" => "vasilakisfil", "name" => "Filippos Vasilakis", "avatar" =>
"https://dxxs0s19ll2ye.cloudfront.net/4c657cfe-116d-4f3f-694d-9164e5294e72.jpg"
}
}
Update every 1 hour in batch updates
or update on every change of the association
(user basic info rarely change)
6. Duplicate data in Postgres
It takes some time to update all rows of a large table...
Be ready for huge concenssions when it comes to performance
Have I talked about any API spec?
JSONAPI, HAL, Sirien etc..
(ok I gave some examples on JSONAPI)
{
"video": {
"id": 3031,
"title": "Leon på havet!",
"description": " #bohuslän",
"state": "published",
"file": "https://d2tyds7k2z0x7k.cloudfront.net/4bc54b39-a869-42ad-9d2e-d7c0ed49f3d3.mp4",
"thumbnail": "https://d2rgaput4dpbfs.cloudfront.net/f8b7a960-698c-ca05-d913-f77f832bf118_custom.jpg",
"links": {
"self": "/api/v1/videos/3031",
"comments": "/api/v1/videos/3031/comments",
"user": "/api/v1/users/18"
},
"likes": [{
"user_id": 18,
"video_id": 3031,
"user": {
"id": 18,
"username": "robert",
"name": "Robert M",
"avatar": "https://dxxs0s19ll2ye.cloudfront.net/4c657cfe-116d-4f3f-694d-9164e5294e72.jpg"
},
"links":{
"video": "/api/v1/videos/3031",
"user": "/api/v1/users/18"
}
}],
"requester_has_liked": false //cached using ActiveCash
}
}
Do we need all those links?
Has anyone
changed a link for a
SINGLE
unique
only
one
resource?!
Thanks!
Questions ?
Blog posts:
https://labs.kollegorna.se/blog/2014/11/rails-api/
https://labs.kollegorna.se/blog/2015/01/ember-overview/
https://labs.kollegorna.se/blog/2015/02/active-hash-relation/
https://labs.kollegorna.se/blog/2015/04/build-an-api-now/
Gems:
https://github.com/elabs/pundit
https://github.com/rails-api/active_model_serializers
https://github.com/kollegorna/active_hash_relation
https://github.com/kollegorna/mongoid_hash_query
https://github.com/kollegorna/rspec-api_helpers
https://github.com/kollegorna/rack-prefer
https://github.com/vasilakisfil/api_bomb
https://github.com/vasilakisfil/active_cash