Skip to content
AgileLogics
6 min read

Designing and Publishing a Gem

Prerequisites: Gems and Bundler, Code organization and RuboCop

Packaging code as a gem lets you reuse it across projects — or share it with the whole community on rubygems.org.

Generate the skeleton

bundle gem slugify_plus --test=minitest --linter=rubocop --ci=github
slugify_plus/
├── slugify_plus.gemspec    # name, version, summary, dependencies
├── Gemfile                 # development dependencies
├── lib/
│   ├── slugify_plus.rb     # entry point: require "slugify_plus"
│   └── slugify_plus/
│       └── version.rb      # VERSION = "0.1.0"
├── test/
├── README.md
└── CHANGELOG.md

Write the library

# lib/slugify_plus.rb
# frozen_string_literal: true

require_relative "slugify_plus/version"

module SlugifyPlus
  class Error < StandardError; end

  def self.call(text, separator: "-")
    raise Error, "text must be a String" unless text.is_a?(String)

    text.unicode_normalize(:nfkd)
        .gsub(/[^\x00-\x7F]/, "")
        .downcase
        .gsub(/[^a-z0-9]+/, separator)
        .delete_prefix(separator)
        .delete_suffix(separator)
  end
end

p SlugifyPlus.call("Héllo, Wörld! Ruby 3.4")
"hello-world-ruby-3-4"

Library design guidelines

  • Namespace everything under one module named after the gem, and define a base Error class so users can rescue all your errors at once.
  • Keep the public API small. Everything public is a promise; mark helpers private (or private_constant for internal classes).
  • Don't monkey patch core classes, and don't print to stdout — return values, raise errors, and accept a logger if logging is needed.
  • Minimize runtime dependencies. Each one becomes a dependency of every app that uses your gem.
  • Use semantic versioning: MAJOR.MINOR.PATCH. Bump MAJOR for breaking changes, MINOR for new features, PATCH for fixes. Record every change in CHANGELOG.md.

Dependencies in the gemspec

spec.required_ruby_version = ">= 3.2"
spec.add_dependency "zeitwerk", "~> 2.6"    # needed at runtime

Development-only gems (test framework, linter) go in the Gemfile. For a library, the Gemfile.lock is not what users get — they resolve versions from your gemspec constraints — so keep those constraints reasonably loose.

Release

bundle exec rake test rubocop
gem build slugify_plus.gemspec
gem push slugify_plus-0.1.0.gem

Or bundle exec rake release, which also tags the version in git. Enable multi-factor authentication on your rubygems.org account — published gems are a popular target for supply-chain attacks.