{"id":44709945,"url":"https://github.com/swiknaba/kirei","last_synced_at":"2026-05-03T22:03:28.511Z","repository":{"id":214673670,"uuid":"686373153","full_name":"swiknaba/kirei","owner":"swiknaba","description":"Kirei is a strictly typed Ruby micro/REST-framework for building scaleable and performant APIs.","archived":false,"fork":false,"pushed_at":"2026-04-26T19:46:55.000Z","size":2529,"stargazers_count":7,"open_issues_count":8,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-26T21:24:13.074Z","etag":null,"topics":["framework","gem","hacktoberfest","ruby"],"latest_commit_sha":null,"homepage":"https://github.com/swiknaba/kirei","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/swiknaba.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2023-09-02T15:16:47.000Z","updated_at":"2026-04-26T19:47:00.000Z","dependencies_parsed_at":"2026-03-21T16:03:48.046Z","dependency_job_id":null,"html_url":"https://github.com/swiknaba/kirei","commit_stats":null,"previous_names":["swiknaba/kirei"],"tags_count":25,"template":false,"template_full_name":null,"purl":"pkg:github/swiknaba/kirei","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiknaba%2Fkirei","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiknaba%2Fkirei/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiknaba%2Fkirei/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiknaba%2Fkirei/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/swiknaba","download_url":"https://codeload.github.com/swiknaba/kirei/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiknaba%2Fkirei/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32586189,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T06:36:36.687Z","status":"ssl_error","status_checked_at":"2026-05-03T06:36:09.306Z","response_time":103,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["framework","gem","hacktoberfest","ruby"],"created_at":"2026-02-15T12:04:26.590Z","updated_at":"2026-05-03T22:03:28.496Z","avatar_url":"https://github.com/swiknaba.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Kirei\n\nKirei is a strictly typed Ruby micro/REST-framework for building scalable and performant APIs. It is built from the ground up to be clean and easy to use. Kirei is based on [Sequel](https://github.com/jeremyevans/sequel) as an ORM, [Sorbet](https://github.com/sorbet/sorbet) for typing, and [Rack](https://github.com/rack/rack) as web server interface. It strives to have zero magic and to be as explicit as possible.\n\nKirei's main advantages over other frameworks are its strict typing, low memory footprint, and built-in high-performance logging and pluggable metric-tracking toolkit. It is opinionated in terms of tooling, allowing you to focus on your core-business. It is a great choice for building APIs that need to scale.\n\n\u003e Kirei (きれい) is a Japanese adjective that primarily means \"beautiful\" or \"pretty.\" It can also be used to describe something that is \"clean\" or \"neat.\"\n\n👉 AI-generated wiki available on [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/swiknaba/kirei)\n\n## Why another Ruby framework?\n\nTL;DR:\n\n* **zero magic**\n* **strict typing**\n* **very few low level dependencies**\n* low memory footprint\n* high performance\n* simple to understand\n\n## Versioning\n\nThis gem follows SemVer, however only after a stable release 1.0.0 is made.\n\nA changelog is maintained via the [GitHub Releases page](https://github.com/swiknaba/kirei/releases).\n\n## Installation\n\nVia rubygems:\n\n```ruby\ngem 'kirei'\n```\n\nTest the latest version via git:\n\n```ruby\ngem 'kirei', git: 'git@github.com:swiknaba/kirei', branch: :main\n```\n\n## Usage\n\n### Initial Set Up\n\nScaffold a new project:\n\n```shell\nbundle exec kirei new \"MyApp\"\n```\n\n### Quick Start\n\nFind a test app in the [spec/test_app](spec/test_app) directory. It is a fully functional example of a Kirei app.\n\n#### Models\n\nAll models must inherit from `T::Struct` and include `Kirei::Model`. They must implement `id` which must hold the primary key of the table. The primary key must be named `id` and be of type `T.any(String, Integer)`.\n\nKirei models are immutable by convention - all properties are defined using `const` and updating a record returns a new instance rather than mutating the original. This immutability, combined with strict typing, makes them naturally suitable for both traditional data-centric applications and domain-driven design approaches.\n\nIn a domain-driven design, `Kirei::Model` serves as the persistence layer, while domain concepts are expressed through `Entity` and `ValueObject`. The domain layer might combine or transform data from multiple models to match the domain's understanding, keeping the internal data structure separate from the public interface.\n\n```ruby\nclass User \u003c T::Struct\n  extend T::Sig\n  include Kirei::Model\n\n  const :id, T.any(String, Integer)\n  const :name, String\nend\n\nuser = User.find_by({ name: 'John' }) # T.nilable(User)\nusers = User.where({ name: 'John' })  # T::Array[User]\n```\n\nUpdating a record returns a new instance. The original instance is not mutated:\n\n```ruby\nupdated_user = user.update({ name: 'Johnny' })\nuser.name         # =\u003e 'John'\nupdated_user.name # =\u003e 'Johnny'\n```\n\nDelete keeps the original object intact. Returns `true` if the record was deleted. Calling delete multiple times will return `false` after the first (successful) call.\n\n```ruby\nsuccess = user.delete # =\u003e T::Boolean\n\n# or delete by any query:\nUser.query.where('...').delete # =\u003e Integer, number of deleted records\n```\n\nTo build more complex queries, Sequel can be used directly:\n\n```ruby\nquery = User.query.where({ name: 'John' })\nquery = query.where('...') # \"query\" is a 'Sequel::Dataset' that you can chain as you like\nquery = query.limit(10)\n\nusers = User.resolve(query)            # T::Array[User]\nfirst_user = User.resolve_first(query) # T.nilable(User)\n\n# you can also cast the raw result manually\nfirst_user = User.from_hash(query.first.stringify_keys)\n```\n\n#### Domain Objects\n\nKirei provides support for Domain-Driven Design patterns through `Kirei::Domain::Entity` and `Kirei::Domain::ValueObject`. Here's how to use them:\n\n```ruby\n# An Entity is identified by its ID\nclass Flight \u003c T::Struct\n  include Kirei::Domain::Entity\n\n  const :id, Integer\n\n  const :flight_number, String\n  const :departure_airport_id, Integer\n  const :arrival_airport_id, Integer\n  const :scheduled_departure_at, Time\n  const :status, String\n\n  sig { returns(T::Boolean) }\n  def can_board?\n    Time.now.utc \u003c= scheduled_departure_at \u0026\u0026 status == 'on_time'\n  end\nend\n\n# A Value Object is identified by its attributes\n# I.e. two Coordinates with the same lat/long will be equal\n# regardless of object identity\nclass Coordinates \u003c T::Struct\n  include Kirei::Domain::ValueObject\n\n  const :latitude, Float\n  const :longitude, Float\n\n  sig { returns(String) }\n  def to_s\n    \"#{latitude},#{longitude}\"\n  end\n\n  sig { params(other_coords: Coordinates).returns(Float) }\n  def distance_to(other_coords)\n    # Implement e.g. Haversine here.\n  end\nend\n\n# Usage example\ncoords = Coordinates.new(latitude: 37.6188, longitude: -122.3750)\ncoords2 = Coordinates.new(latitude: 37.6188, longitude: -122.3750)\n\ncoords == coords2 # true\n\nflight = Flight.new(id: 123, flight_number: 'UA123', status: 'on_time', scheduled_departure_at: Time.now.utc)\nflight2 = Flight.new(id: 123, flight_number: 'UA123', status: 'delayed', scheduled_departure_at: Time.now.utc)\n\nflight == flight2 # true - same entity even with different status\n```\n\n#### Database Migrations\n\nRead the [Sequel Migrations](https://github.com/jeremyevans/sequel/blob/5.78.0/doc/schema_modification.rdoc) documentation for detailed information.\n\n```ruby\nSequel.migration do\n  up do\n    create_table(:airports) do\n      primary_key :id\n      String :name, null: false\n    end\n  end\n\n  down do\n    drop_table(:airports)\n  end\nend\n```\n\nApplying migrations:\n\n```shell\n# create the database\nbundle exec rake db:create\n\n# drop the database\nbundle exec rake db:drop\n\n# apply all pending migrations\nbundle exec rake db:migrate\n\n# annotate the models with the schema\n# this runs automatically after each migration\nbundle exec rake db:annotate\n\n# roll back the last n migration\nSTEPS=1 bundle exec rake db:rollback\n\n# run db/seeds.rb to seed the database\nbundle exec rake db:seed\n\n# scaffold a new migration file\nbundle exec rake 'db:migration[CreateAirports]'\n```\n\n#### Routing\n\nDefine routes anywhere in your app; by convention, they are defined in `config/routes.rb`:\n\n```ruby\n# config/routes.rb\n\nmodule Kirei::Routing\n  Router.add_routes(\n    [\n      Route.new(\n        verb: Verb::GET,\n        path: \"/livez\",\n        controller: Controllers::Health,\n        action: \"livez\",\n      ),\n      Route.new(\n        verb: Verb::GET,\n        path: \"/airports\",\n        controller: Controllers::Airports,\n        action: \"index\",\n      ),\n      Route.new(\n        verb: Verb::GET,\n        path: \"/airports/:iata\",\n        controller: Controllers::Airports,\n        action: \"show\",\n      ),\n    ],\n  )\nend\n```\n\n#### Controllers\n\nControllers can be defined anywhere; by convention, they are defined in the `app/controllers` directory.\n\nThree render helpers are available:\n\n| Method | Use case |\n|---|---|\n| `render(body, status:, headers:)` | Raw string responses (plain text, pre-serialized data) |\n| `render_json(data, status:, headers:)` | JSON responses with automatic serialization |\n| `render_error(errors, status:, headers:)` | JSON:API-compliant error responses |\n\n`render_json` accepts multiple data types:\n\n| `data` type | Behavior |\n|---|---|\n| `String` | Pass-through (assumed to be pre-serialized JSON) |\n| `Hash` / `Array` | Serialized via `Oj.dump` |\n| Object responding to `#serialize` (e.g. `T::Struct`) | Calls `.serialize`, then `Oj.dump` if the result is not a String |\n| Anything else | Raises `ArgumentError` |\n\n```ruby\nmodule Controllers\n  class Airports \u003c Kirei::Controller\n    extend T::Sig\n\n    sig { returns(T.anything) }\n    def index\n      search = T.let(params.fetch(\"q\", nil), T.nilable(String))\n\n      service = Kirei::Services::Runner.call(\"Airports::Filter\") do\n        Airports::Filter.call(search)\n      end\n      return render_error(service.errors, status: 400) if service.failed?\n\n      render_json(service.result.map(\u0026:serialize))\n    end\n\n    sig { returns(T.anything) }\n    def show\n      iata = T.must(params.fetch(\"iata\", nil)) # named param from dynamic route\n\n      airport = Kirei::Services::Runner.call(\"Airports::Find\") do\n        Airports::Find.call(iata) # T.nilable(Airport)\n      end\n      return render(status: 204) if airport.nil?\n\n      render_json(airport) # T::Struct — calls .serialize automatically\n    end\n  end\nend\n```\n\nServices can be PORO. You can wrap an execution in `Kirei::Services::Runner` which will emit a standardized logline and track its execution time.\n\n```ruby\nmodule Airports\n  class Filter\n    extend T::Sig\n\n    sig do\n      params(\n        search: T.nilable(String),\n      ).returns(T::Array[Airport])\n    end\n    def self.call(search)\n      return Airport.all if search.nil?\n\n      #\n      # SELECT *\n      # FROM \"airports\"\n      # WHERE ((\"name\" ILIKE 'xx%') OR (\"id\" ILIKE 'xx%'))\n      #\n      query = Airport.query.where(Sequel.ilike(:name, \"#{search}%\"))\n      query = query.or(Sequel.ilike(:id, \"#{search}%\"))\n\n      Airport.resolve(query)\n    end\n  end\nend\n```\n\n#### Metrics\n\nKirei ships with a pluggable metrics interface via `Kirei::Metrics::Backend`. Three backends are included:\n\n| Backend | Description |\n|---|---|\n| `LoggingBackend` | **Default.** Prints metrics to stdout via `puts` — great for local development and small MVPs. |\n| `StatsdBackend` | Wraps [`statsd-instrument`](https://github.com/Shopify/statsd-instrument). Add `gem 'statsd-instrument'` to your Gemfile. |\n| `NullBackend` | No-op — silently discards all metrics. |\n\nThe backend exposes three methods: `increment`, `measure`, and `gauge`.\n\nConfigure the backend in your app:\n\n```ruby\nclass MyApp \u003c Kirei::App\n  # Use StatsD (requires `gem 'statsd-instrument'` in Gemfile)\n  config.metrics_backend = Kirei::Metrics::StatsdBackend.new\n\n  # Or disable metrics entirely\n  config.metrics_backend = Kirei::Metrics::NullBackend.new\nend\n```\n\nEmit custom metrics anywhere via `Kirei::Logging::Metric`:\n\n```ruby\nKirei::Logging::Metric.call(\"airports_search_term\", 1, tags: { \"query\" =\u003e search })\n```\n\nRequest timing and service execution timing are tracked automatically.\n\nTo build a custom backend (e.g. Prometheus, OpenTelemetry), subclass `Kirei::Metrics::Backend` and implement `increment`, `measure`, and `gauge`.\n\n### Goes well with these gems\n\n* [pagy](https://github.com/ddnexus/pagy) for pagination\n* [argon2](https://github.com/technion/ruby-argon2) for password hashing\n* [rack-session](https://github.com/rack/rack-session) for session management\n* [pgvector](https://github.com/pgvector/pgvector-ruby) for vector columns — add `:pgvector` to `App.config.db_extensions`\n\n### Middlewares\n\nIf you place custom middlewares under `config/middleware/*.rb`, they will be automatically loaded, see [app.rb](spec/test_app/app.rb) \"load configs\".\n\nHowever, you still need to `use` them in the `config.ru` file, see the generated [config.ru](spec/test_app/config.ru) file, this gives you full control over the order in which middlewares are executed:\n\n```ruby\n# Load middlewares here\nuse(Rack::Reloader, 0) if TestApp.environment == \"development\"\n\n# add more custom middlewares here, e.g.\nuse(Middleware::Example)\n\n# Launch the app\nrun(TestApp.new)\n```\n\nMiddleware provided by a gem like [rack-session](https://github.com/rack/rack-session) must be added to the `config.ru` file as well if you want to use it.\n\n## Contributions\n\nWe welcome contributions from the community. Before starting work on a major feature, please get in touch with us either via email or by opening an issue on GitHub. \"Major feature\" means anything that changes user-facing features or significant changes to the codebase itself.\n\nPlease commit small and focused PRs with descriptive commit messages. If you are unsure about a PR, please open a draft PR to get early feedback. A PR must have a short description (\"what\"), a motiviation (\"why\"), and, if applicable, instructions how to test the changes, measure performance improvements, etc.\n\n## Publishing a new version\n\nrun\n\n```shell\nbin/release\n```\n\nwhich will guide you through the release process.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswiknaba%2Fkirei","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fswiknaba%2Fkirei","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswiknaba%2Fkirei/lists"}