In addition to the app directory, Hanami also supports organising your application code into slices.

You can think of slices as distinct modules of your application. A typical case is to use slices to separate your business domains (for example billing, accounting or admin) or to have separate modules for a particular feature (API) or technical concern (search).

Slices exist in the slices directory.

Creating a slice

Hanami provides a slice generator. To create an API slice, run bundle exec hanami generate slice api.

This creates a directory in slices, adding some slice-specific classes like actions:

  1. bundle exec hanami generate slice api
  2. slices
  3. └── api
  4. ├── action.rb
  5. └── actions

Simply creating a new directory in slices will also create a slice:

  1. mkdir -p slices/admin
  2. slices
  3. └── admin

Features of a slice

Slices offer much of the same behaviour and features as the Hanami app.

A Hanami slice:

  • has its own container
  • imports a number of standard components from the app
  • can have its own providers (e.g. slices/api/providers/my_provider.rb)
  • can include actions, routable from the application’s router
  • can import and export components from other slices
  • can be prepared and booted independently of other slices
  • can have its own slice-specific settings (e.g. slices/api/config/settings.rb)

Slice containers

Like Hanami’s app folder, components added to a Hanami slice are automatically organised into the slice’s container.

For example, suppose our Bookshelf application, which catalogues international books, needs an API to return the name, flag, and currency of a given country. We can create a countries show action in our API slice (by running bundle exec hanami generate action countries.show --slice api or by adding the file manually) that looks like:

  1. # slices/api/actions/countries/show.rb
  2. require "countries"
  3. module API
  4. module Actions
  5. module Countries
  6. class Show < API::Action
  7. include Deps[
  8. query: "queries.countries.show"
  9. ]
  10. params do
  11. required(:country_code).value(included_in?: ISO3166::Country.codes)
  12. end
  13. def handle(request, response)
  14. response.format = :json
  15. halt 422, {error: "Unprocessable country code"}.to_json unless request.params.valid?
  16. result = query.call(
  17. request.params[:country_code]
  18. )
  19. response.body = result.to_json
  20. end
  21. end
  22. end
  23. end
  24. end

This action uses the countries gem to check that the provided country code (request.params[:country_code]) is a valid ISO3166 code and returns a 422 response if it isn’t.

If the code is valid, the action calls the countries show query (aliased here as query for readability). That class might look like:

  1. # slices/api/queries/countries/show.rb
  2. require "countries"
  3. module API
  4. module Queries
  5. module Countries
  6. class Show
  7. def call(country_code)
  8. country = ISO3166::Country[country_code]
  9. {
  10. name: country.iso_short_name,
  11. flag: country.emoji_flag,
  12. currency: country.currency_code
  13. }
  14. end
  15. end
  16. end
  17. end
  18. end

As an exercise, as with Hanami.app and its app container, we can boot the API::Slice to see what its container contains:

  1. bundle exec hanami console
  2. bookshelf[development]> API::Slice.boot
  3. => API::Slice
  4. bookshelf[development]> API::Slice.keys
  5. => ["settings",
  6. "actions.countries.show",
  7. "queries.countries.show",
  8. "inflector",
  9. "logger",
  10. "notifications",
  11. "rack.monitor",
  12. "routes"]

We can call the query with a country code:

  1. bookshelf[development]> API::Slice["queries.countries.show"].call("UA")
  2. => {:name=>"Ukraine", :flag=>"🇺🇦", :currency=>"UAH"}

Standard app components

Since every slice is part of the larger app, a number of standard app components are automatically imported into each slice. These include:

  • "settings" — the app’s settings object
  • "inflector" — the app’s inflector
  • "logger" — the app’s logger
  • "routes" — the app’s routes helper

If you have additional components in your app that you wish to make available to each slice, you can configure these via config.shared_app_component_keys:

  1. # config/app.rb
  2. require "hanami"
  3. module Bookshelf
  4. class App < Hanami::App
  5. config.shared_app_component_keys += ["my_app_component"]
  6. end
  7. end

Think carefully before making components available to every slice, since this can create an undesirable level of coupling between the slices and the app. Instead, you may wish to consider slice imports and exports.

Slice imports and exports

Suppose that our bookshelf application uses a content delivery network (CDN) to serve book covers. While this makes these images fast to download, it does mean that book covers need to be purged from the CDN when they change, in order for freshly updated images to take their place.

Images can be updated in one of two ways: the publisher of the book can sign in and upload a new image, or a Bookshelf staff member can use an admin interface to update an image on the publisher’s behalf.

In our bookshelf app, an Admin slice supports the latter functionality, and a Publisher slice the former. Both these slices want to trigger a CDN purge when a book cover is updated, but neither slice needs to know exactly how that’s achieved. Instead, a CDN slice can manage this operation.

  1. # slices/cdn/book_covers/purge.rb
  2. module CDN
  3. module BookCovers
  4. class Purge
  5. def call(book_cover_path)
  6. # "Purging logic here!"
  7. end
  8. end
  9. end
  10. end

Slices can be configured by creating a file at config/slices/slice_name.rb.

To configure the Admin slice to import components from the CDN container (including the purge component above), we can create a config/slices/admin.rb file with the following configuration:

  1. # config/slices/admin.rb
  2. module Admin
  3. class Slice < Hanami::Slice
  4. import from: :cdn
  5. end
  6. end

Let’s see this import in action in the console, where we can see that the Admin slices’ container now has a "cdn.book_covers.purge" component:

  1. bundle exec hanami console
  2. bookshelf[development]> Admin::Slice.boot.keys
  3. => ["settings",
  4. "cdn.book_covers.purge",
  5. "inflector",
  6. "logger",
  7. "notifications",
  8. "rack.monitor",
  9. "routes"]

Using the purge operation from the CDN slice within the Admin slice component below is now as simple as using the Deps mixin:

  1. # slices/admin/books/operations/update.rb
  2. module Admin
  3. module Books
  4. module Operations
  5. class Update
  6. include Deps[
  7. "repositories.book_repo",
  8. "cdn.book_covers.purge"
  9. ]
  10. def call(id, params)
  11. # ... update the book using the book repository ...
  12. # If the update is successful, purge the book cover from the CDN
  13. purge.call(book.cover_path)
  14. end
  15. end
  16. end
  17. end
  18. end

It’s also possible to import only specific components from another slice. Here for example, the Publisher slice imports strictly the purge operation, while also - for reasons of its own choosing - using the suffix content_network instead of cdn:

  1. # config/slices/publisher.rb
  2. module Publisher
  3. class Slice < Hanami::Slice
  4. import keys: ["book_covers.purge"], from: :cdn, as: :content_network
  5. end
  6. end

In action in the console:

  1. bundle exec hanami console
  2. bookshelf[development]> Publisher::Slice.boot.keys
  3. => ["settings",
  4. "content_network.book_covers.purge",
  5. "inflector",
  6. "logger",
  7. "notifications",
  8. "rack.monitor",
  9. "routes"]

Slices can also limit what they make available for export to other slices.

Here, we configure the CDN slice to export only its purge component:

  1. # config/slices/cdn.rb
  2. module CDN
  3. class Slice < Hanami::Slice
  4. export ["book_covers.purge"]
  5. end
  6. end

Slice settings

Every slice having automatic access to the app’s "settings" component is convenient, but for large apps this may lead to those settings becoming unwieldy: the list of settings can become long, and many settings will not be relevant to large portions of your app.

You can instead elect to define settings within specific slices. To do this, create a config/settings.rb within your slice directory.

  1. # slices/cdn/config/settings.rb
  2. module CDN
  3. class Settings < Hanami::Settings
  4. setting :cdn_api_key, Types::String
  5. end
  6. end

With this in place, the "settings" component within your slice will be an instance of this slice-specific settings object.

  1. CDN_API_KEY=xyz bundle exec hanami console
  2. bookshelf[development]> CDN::Slice["settings"].cdn_api_key # => "xyz"

You can then include the slice settings via the Deps mixin within your slice.

  1. # slices/cdn/book_covers/purge.rb
  2. module CDN
  3. module BookCovers
  4. class Purge
  5. include Deps["settings"]
  6. def call(book_cover_path)
  7. # use settings.cdn_api_key here
  8. end
  9. end
  10. end
  11. end

Slice settings are loaded from environment variables just like the app settings, so take care to ensure you have no naming clashes between your slice and app settings.

See the settings guide for more information on settings.

Slice loading

Hanami will load all slices when your app boots. However, for certain workloads of your app, you may elect to load only a specified list of slices.

Loading specific slices brings the benefit of stronger code isolation, faster boot time and reduced memory usage. If your app had a background worker that processed jobs from one slice only, then it would make sense to load only that slice for the worker’s process.

To do this, set the HANAMI_SLICES environment variable with a comma-separated list of slice names.

  1. $ HANAMI_SLICES=cdn,other_slice_here bundle exec your_hanami_command

Setting this environment variable is a shortcut for setting config.slices in your app class.

  1. # config/app.rb
  2. require "hanami"
  3. module Bookshelf
  4. class App < Hanami::App
  5. config.slices = ["cdn"]
  6. end
  7. end

You may find the HANAMI_SLICES environment variable more convenient since it will not disturb slice loading for all other processes running your app.