DEV Community

Cover image for Local API in a single file
Oinak
Oinak

Posted on

Local API in a single file

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)