Local API in a single file
Tags: api, ruby, sinatra, programming
Published: 2024-02-03 19:59:34

_image credit: [pngimg](https://pngimg.com/image/91353)_

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](https://bundler.io/guides/bundler_in_a_single_file_ruby_script.html)

So to begin, we will create a ruby file called `app.rb` and write this:
```ruby
#!/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:
```ruby
require 'sinatra'
get '/frank-says' do
  'Put this in your pipe & smoke it!'
end
```

But I prefer the [modular style](https://sinatrarb.com/intro.html#sinatrabase---middleware-libraries-and-modular-apps). So they look like this:
```ruby
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:
```ruby
# 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__](https://ruby-doc.org/docs/keywords/1.9/Object.html#method-i-__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:
```ruby
  # 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](https://docs.ruby-lang.org/en/3.3/YAML.html) 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:
```bash
$ ./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](https://es.wikipedia.org/wiki/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](https://avdi.codes/about/) says: "Happy Hacking"