Rest Api in crystal lang using Shivneri
Introduction
Crystal is a statically type programming language which is heavily inspired by ruby & features like -
- Concurrency using green threads known as fiber (similar to go routine)
- Object oriented with focus on using struct for better memory management
- Performance like c language
- Package dependency manager using Shards
- Statically type
- Ruby syntax
makes it a powerful language.
In this article - we will learn how to create a rest api in crystal language using Shivneri framework. You can download code of this article from - https://github.com/ujjwalguptaofficial/shivneri-examples/tree/master/rest
Shivneri is a MVC framework for crystal which focuses on good coding styles, modularity and performance.
It provides lots of awesome features which helps you to create enterprise grade application. Some of features are -
- Provides components - Wall, Shield and Guard. Components help modularize the application
- Provides you ways to write clean & maintainable codes
- Dependency Injection
- Everything is configurable — you can configure your session store, view engine, websocket etc
Let’s Code
Before moving further please make sure you have installed crystal. For information about how to install, visit - https://crystal-lang.org/install/
Project Setup
Execute command — crystal init app rest
this will create a folder “rest” which contains folder structure and some files to run a crystal app.
Install Shivneri
Add below code in shard.yml
dependencies:
shivneri:
github: ujjwalguptaofficial/shivneri
This will add shivneri as dependency in your app.
Now install Shivneri by runing command — shards install
Initiate Shivneri
Paste below code in file src/rest.cr
require "shivneri"include Rest# TODO: Write documentation for `Rest`
module Rest
VERSION = "0.1.0" Shivneri.open do
puts "app is started"
end
end
Above code require shivneri and start the app by calling “open” api.
Create folders for Shivneri
Shivneri is a MVC framework which means you need to create a controller for writing your logic, so let’s create a folder controllers inside src.
Now add a file default_controller.cr inside folder controllers & paste below code
module Rest
module Controller class DefaultController < Shivneri::Controller @[DefaultWorker]
def index
text_result("Welcome to Shivneri")
end end end
end
Oh, so many new things right ? Worry not - let’s first list what are new keywords and then i will explain each of them.
- Shivneri::Controller
- DefaultWorker
- text_result
Shivneri::Controller
The class “DefaultController” is inheriting class “Shivneri::Controller” which makes - “DefaultController” a controller and now it can access http request data like query string, post data etc.
DefaultWorker
DefaultWorker is a special annotation which marks method index as worker & also add route “/” with http methods “GET”. This means that now you can call method “index” on route “/” and http method “GET” only.
Methods which is used as end point are called Worker in Shivneri, because they do some kind of work and return result.
text_result
text_result is a method which is used to return http result of type plain text.
Similar to text_result there are other methods available which is used to return different type of results like -
- json_result - used to return result of type json
- html_result - used to return result of type html
- file_result - used to return a file.
So if we will summarize above code - the method “index” returns plain text “Welcome to Shivneri” and it can be accessed using route “/” and http method “GET” only.
Can we access our controller from a url now ?
no - we still need to map our controller with some route, so that it can be accessed using url.
Mapping Controller
create a file routes.cr inside src folder & paste below code
require "./controllers/default_controller"module Rest include Controller def self.routes
return [
{
controller: DefaultController,
path: "/*",
}
]
end
end
as you can see - we are importing “DefaultController” and mapping with a path “/*” .
/* means for any route after “/” unless explicitly defined
now we need to provide variable routes from above code to Shivneri. Let’s import this in rest.cr and provide to Shivneri -
require "shivneri"
require "./routes"include Rest# TODO: Write documentation for `Rest`
module Rest
VERSION = "0.1.0" Shivneri.routes = routes Shivneri.open do
puts "app is started"
end
end
I hope you are able to understand these implementation so far, if not please feel free to ask in comment section.
Now we have created a route which can be accessed using url. Let’s run our program and test
- Execute command - crystal src/rest.cr
- Send a get http request to url - http://localhost:4000/
Hooray! , we have successfully created an end point.
REST
Let’s create another controller “UserController” which will implement- get user , add user, update user & delete user.
Create a file user_controller.cr inside controllers folders and paste below code
module Rest
module Controller
class UserController < Shivneri::Controller
end
end
end
Let’s import this controller in routes and map with a path “/user”
require "./controllers/default_controller"
require "./controllers/user_controller"module Rest include Controller def self.routes
return [
{
controller: DefaultController,
path: "/*",
},
{
controller: UserController,
path: "/user",
},
]
end
end
Add User
We will have to create a method which will take user data from post request and add user.
For storing users - we will create a static variable which will stores all user. Let’s implement add_user
module Rest
module Controller
class UserController < Shivneri::Controller
@@users = [] of NamedTuple(id: Int32, name: String, gender: String)
@@last_user_id = 0; @[Worker("POST")]
@[Route("/")]
def add_user new_user = {
name: body["name"].as_s,
gender: body["gender"].as_s,
id: @@last_user_id += 1
} @@users.push(new_user) return json_result(new_user, 201) end
end
end
end
In above code -
- We have created two static variable-
users
andlast_user_id
. users maintain list of user & last_user_id is used for tracking last id of user. - We have created a method add_user which is a worker method. We have marked it as worker using annotation “Worker”.
- We are restricting this worker to only post request by passing only “POST” method in Worker annotation. It means that when http method will be post then only this worker will be called.
- We have changed router of “add_user” to “/” method by using annotation “Route”. If we won’t change route then by default route is method name of worker.
- We are extracting data from body and storing it in a variable new_user.
- We are adding new_user variable in our users stores.
- In the end we are returning added user as json result using method “json_result” with http status code 201 (resource created).
Let’s test this by sending a post http request to end point - http://localhost:4000/user
In our implementation we are not doing any validation which means any one can store garbage or invalid value in our database.
Shivneri provides Guard component for validation on worker level. Please read Guard doc and implement validation in “add_user” method.
Get User
The get user will return a user info by its id. Let’s implement this
@[Worker("GET")]
@[Route("/{user_id}")]
def get_user
user_id = param["user_id"].to_i
user = @@users.find{|u| u[:id] == user_id}
if (user != nil)
return json_result(user)
else
return text_result("invalid user", 404)
end
end
In above code
- We have added a method “get_user” as worker.
- We are restricting this worker to only GET request by passing only “GET” method in Worker annotation.
- We have changed router of worker to “/{user_id}” using annotation Route. The route contains a variable “user_id” which will hold the user_id value from route.
- In method - we are extracting user id from route param and checking if any user exist for that id. If user is found then we return user as json result otherwise return text result “invalid user” with status code 404.
Let’s test our end point by sending a get request to url - http://localhost:4000/user/1
Update User
The update user implementation will take user info from body and then update user.
@[Worker("PUT")]
@[Route("/")]
def update_user
user = body.to_tuple(NamedTuple(id: Int32, name: String, gender: String));
index_of_saved_user = @@users.index { |q| q[:id] == user[:id] }
if (index_of_saved_user == nil)
return text_result("invalid user", 404)
else
@@users[index_of_saved_user.as(Int32)] = user
return text_result("user updated")
end
end
In above code
- We have created a method “update_user” and marked it as worker.
- We are restricting this worker to only PUT request by passing only “PUT” method in Worker annotation.
- We have changed router of worker to “/” using annotation Route.
- In method - we are extracting user from body.
Important point to note is that we are using a method “to_tuple” which takes a named tuple type & returns a named tuple value. It saves us from extracting every value and then converting it into a type manually.
- We are checking if a user exist and if exist then update the user and return text result “user updated” .
- If user does not exist then we return text result “invalid user” with status code 404 .
Let’s test our implementation by sending a PUT request to url- http://localhost:4000/user
Remove User
Remove user will remove a user by its id.
@[Worker("DELETE")]
@[Route("/{user_id}")]
def remove_user user_id = param["user_id"].to_i
index_of_saved_user = @@users.index{|u| u[:id] == user_id} if (index_of_saved_user != nil)
@@users.delete_at(index_of_saved_user.as(Int32))
return text_result("user deleted")
else
return text_result("invalid user", 404)
end
end
In above code
- We have added a method “remove_user” as worker.
- We are restricting this worker to only DELETE request by passing only “DELETE” method in Worker annotation.
- We have changed router of worker to “/{user_id}” using annotation Route. The route contains a variable “user_id” which will hold the user_id value from route.
- In method - we are extracting user id from route param and checking if any user exist for that id. If user is found then we delete that user & return message “user deleted” as text result otherwise return text result “invalid user” with status code 404.
Let’s test our end point by sending a DELETE request to url — http://localhost:4000/user/1
Finally we have successfully implemented REST api for resource user. I hope you have enjoyed this article, please clap and share if you liked it.
Thanks!