I development API with Grape gem. I found a style or said pattern which works great and decide to write it down.
There are five steps in an API call: authentication
, validation
, authorization
, operation
and presentation
. Every step has its own responsibility and we can organize code more structurally. I will use PostAPI to describe each step.
Authentication
Authenticate user before entering endpoint. If user not authenticated, response 401.
Validation
Validate request params. If validation failed, response 400. GET
, PATCH
, DELETE
request need to find the resource first. If not found, response 404.
If Grape default validator is not enough, customize new validator or use form object to do the validation.
Authorization
Check the policy of the resource. For example: maybe only paid user can create post or user can post at most 2 posts every day. If failed, response 403.
Pundit, cancancan are great gems to handle this.
Operation
Create, read, update or delete a resource, any business logic about the resource. Prepare the resource for the next step. If any error happened, response the correspond http status code.
Presentation
Serialize resource. Only logic for transforming format of the resource, such as post.created_at.strftime('%Y/%m/%d')
.
class PostAPI < Grape::API
rescue_from BadRequestError do { # 400 }
rescue_from NotAuthenticatedError do { # 401 }
rescue_from NotAuthorizedError do { # 403 }
rescue_from NotFoundError do { # 404 }
rescue_from StandardError do { # 500 }
resource 'posts' do
# authentication
before { authenticate_user! }
get ':id' do
# validation and operation
post = current_user.posts.find(params[:id])
# presentation
present post, with: PostEntity
end
# validation
params do
requires :title, PostEntity.documentation[:title]
end
post do
# authorization
authorize current_user, PostPolicy, :create?
# operation
post = Post.new(title: params[:title])
error!('Duplicate post', 409) if post.duplicate?
post.save
# presentation
present post, with: PostEntity
end
end
end
class PostPolicy
def create?
current_user.paid?
end
end
class PostEntity < Grape::Entity
expose :title, documentation: { type: String, allow_blank: false }
expose :created_at
def created_at
object.created_at.strftime('%Y/%m/%d')
end
end