image credit: pngimg
Sometimes yo need to develop an API client for a service that you cannot call. I.e. if payments or modification of delicate data are to happen when you make calls. Some APIs hae a test or sandbox environment that you can call instead, like Stripe, but it is not always the case.
Another scenario in which you might want a dummy API is if you are developing both the client and the server, but you don't have the server yet. If you are looking for market fit of a prototype, starting on the client/UI makes a lot of sense, as it is much easier to ask potential users and or investors about something they can see/touch.
For the most basic cases like users, posts or products, there are numerous services online you can hook to.
With that out of the way, if you need or just want your own API server, be it because you want more control on the behaviour, response content, or don't want to depend on connectivity, here is a way you can do it.
Self contained dependencies
We are going to take advantage of a Bundler feature that allows you to include a dependency declaration within your ruby script.
Here you can find the documentation
So to begin, we will create a ruby file called app.rb
and write this:
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'bundler/inline'
gemfile(true) do
source 'https://rubygems.org'
gem 'rackup', '~> 2.1' # sinatra needs this to serve
gem 'sinatra', '~> 4.0'
gem 'sinatra-contrib', '~> 4.0'
end
Single class API
We are going to use Sinatra-rb, a very small but powerful ruby framework.
You can defin a sinatra app with something like:
require 'sinatra'
get '/frank-says' do
'Put this in your pipe & smoke it!'
end
But I prefer the modular style. So they look like this:
class App < Sinatra::Base
get '/' do
"Hello"
end
# run server if you run the script by name
run! if app_file == $PROGRAM_NAME
end
You may have noticed that I included sinatra-contrib
gem earlier. It is a compendium of commonly used extras for Sinatra, among them, nice JSON suport.
Your API can do what ever, you should explore Sinatra's docs to find what you need, but I will include a little example here:
# inline bundler section you saw before
require 'sinatra/base'
require 'sinatra/json'
# Dummy API
class App < Sinatra::Base
get '/' do
'Use "GET /orders.json?page=N" to get order data'
end
get '/orders.json' do
orders, page, total_pages = paginate(order_data)
redirect to('/not_found') if page > total_pages
json({ orders:, page:, total_pages: })
end
not_found do
status 404
json({ error: 'Not found' })
end
def paginate(list)
page = (request.params['page'] || '1').to_i
per = (request.params['per'] || '3').to_i
total_pages = (order_data.size / per.to_f).ceil
return [[], page, total_pages] unless (1..total_pages).include? page
[list[per * (page - 1), per], page, total_pages]
end
def order_data
# ... coming later
end
run! if app_file == $PROGRAM_NAME
end
In this example I included a single get endpoint returning a list of an "order" resource, and I implemented a very naif pagination just in case the client needs to deal with that.
This is by no mean the only way to do so, you could have, all pages return the same result and just change the pagination attributes or whatever. Explore what you need and adapt to suit your use case.
Embeded data
There is a slightly mysterious bit on the code above where the order_data
method is not shown. That is because I want to tell you about another nifty feature of Ruby regarding single-file scripts.
In ruby you can use END to mark the end of the normal source file. Ruby won't run anything bellow that line, but the rest of the contents of the file will be available as the special IO object DATA
.
So the end of my script looks something like this:
# the rest of the file you have already seen
def order_data
@@order_data ||= YAML.load(DATA.read)
end
run! if app_file == $PROGRAM_NAME
end
__END__
---
- date: 2020-01-01T09:00Z
customer_id: 1
order_lines:
- product_id: 1
units: 2
unit_price: 1000
- product_id: 3
units: 1
unit_price: 499
- date: 2020-01-02T09:03Z
customer_id: 2
order_lines:
- product_id: 2
units: 1
unit_price: 499
- date: 2020-01-04T11:20Z
customer_id: 2
order_lines:
- product_id: 2
units: 1
unit_price: 499
- date: 2020-01-13T15:00Z
customer_id: 3
order_lines:
- product_id: 1
units: 2
unit_price: 1000
- product_id: 3
units: 1
unit_price: 999
- date: 2020-01-13T15:05Z
customer_id: 2
order_lines:
- product_id: 1
units: 1
unit_price: 1000
- product_id: 2
units: 2
unit_price: 499
There are a couple of interesting bits here:
The data bellow __END__
is in YAML format. YAML is a serialization language very common in ruby world. Ruby on rails config files, for instance, are written in YAML.
You can find YAML supported on Ruby's standard library, but you will need to throw a require 'yaml'
to have the YAML
object available.
In this case I could have used JSON directly, but YAML is friendlier for humans to edit unless you love writing zillions of {
s,"
s, and }
s.
Another thing is happening, is that I am using a class variable @@order_data
, to memoize this information. This is to avoid having to read if every request (Sinatra uses a new instance per request, but the class data lives while the server is running).
Conclusions
There you have it. i have shared with you three tools that combined let you define a Single File Local API stand-in that you can run anywhere you have Ruby and bundler
as easy as doing:
$ ./app.rb
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...
[2024-02-03 20:45:07] INFO WEBrick 1.8.1
[2024-02-03 20:45:07] INFO ruby 3.3.0 (2023-12-25) [x86_64-linux]
== Sinatra (v4.0.0) has taken the stage on 4567 for development with backup from WEBrick
[2024-02-03 20:45:07] INFO WEBrick::HTTPServer#start: pid=124069 port=4567
With the #!/usr/bin/env ruby
on the first line (it is called a shebang) you don't even need to specify Ruby as the interpreter on most environments.
I hope you have found this useful, or at least, entertaining.
Please let me know what fun things you do with it.
As the wise Avdi says: "Happy Hacking"
Top comments (0)