{"version":"https://jsonfeed.org/version/1","title":"Hashrocket","home_page_url":"https://hashrocket.com/blog","feed_url":"https://hashrocket.com/blog.json","author":{"name":"Hashrocket","url":"https://hashrocket.com/","avatar":"https://hashrocket.com/favicon-228.png"},"items":[{"id":"https://hashrocket.com/blog/posts/testing-readonly-models","url":"https://hashrocket.com/blog/posts/testing-readonly-models","title":"Testing Readonly Models","content_html":"I was working with a readonly model in Rails the other day and ran into an issue whilst testing it. Here's what I ran into and the solution I came up with.\n\nReadonly models are a great way to signal that, well, you should only ever read them, not write them. Maybe you have some external system that connects to the database for writes, or maybe your Rails app connects to some data warehouse for some queries or reports. It's can be useful to have a safeguard to prevent accidental errant writes. It's actually really simple to make a model readonly, you just need to override the [`readonly?`](https://api.rubyonrails.org/classes/ActiveRecord/Core.html#method-i-readonly-3F) method:\r\n\r\n``` ruby\r\nclass ReadOnlyPost \u003c ApplicationRecord\r\n  def readonly? = true\r\nend\r\n```\r\n\r\nNow any attempt to create/save/update/delete a `ReadOnlyPost` will raise a friendly `ActiveRecord::ReadOnlyRecord` exception.\r\n\r\n## The Problem\r\n\r\nYou might be able to see where this is going. \r\n\r\nFor any tests that can avoid saving this readonly model to the database, (i.e. using `ReadOnlyPost.new` or `FactoryBot.build`), then we're all good. But often tests need to persist some records to the test database. And if I try to create a `ReadOnlyPost`, I'm going to have a bad time.\r\n\r\n``` ruby\r\nRSpec.describe ReadOnlyPost, type: :model do\r\n  let(:post) { ReadOnlyPost.create(title: \"Title\", body: \"body\") }\r\n\r\n  it \"can create a post\" do\r\n    expect(post).to be_a(ReadOnlyPost)\r\n  end\r\nend\r\n```\r\n``` shell\r\n% bundle exec rspec\r\nF\r\n\r\nFailures:\r\n\r\n  1) ReadOnlyPost can create a post\r\n     Failure/Error: let(:post) { ReadOnlyPost.create(title: \"Title\", body: \"body\") }\r\n\r\n     ActiveRecord::ReadOnlyRecord:\r\n       ReadOnlyPost is marked as readonly\r\n     # ./spec/models/read_only_post_spec.rb:4:in `block (2 levels) in \u003ctop (required)\u003e'\r\n     # ./spec/models/read_only_post_spec.rb:7:in `block (2 levels) in \u003ctop (required)\u003e'\r\n```\r\n\r\nThis makes sense, the readonly property of the model doesn't go away in test - creating a record is creating a record is writing to the database, so it will fail.\r\n\r\n## The Solution\r\n\r\nWhat we want to do is override this constraint to temporarily allow us to persist data. Ideally, we do this very narrowly to not impact the system under test. If the model is writable during test execution, then the behavior in the test is different from production behavior and that can lead to bugs in production that we cannot capture in a test. So ideally we can override this during test setup only and revert back to readonly by the time the test executes.\r\n\r\nGiven those constraints, a nice API would look like this:\r\n\r\n``` ruby\r\nRSpec.describe ReadOnlyPost, type: :model do\r\n  # In a let\r\n  let(:post) { with_writable(ReadOnlyPost) { ReadOnlyPost.create(title: \"Title\", body: \"body\") } }\r\n\r\n  # Or in a before block\r\n  before do\r\n    with_writable(ReadOnlyPost) do\r\n      ReadOnlyPost.create(title: \"Title\", body: \"body\")\r\n    end\r\n  end\r\n\r\n  it \"can create a post\" do\r\n    # Even inline in a test\r\n    with_writable(ReadOnlyPost) { ReadOnlyPost.create(title: \"Title\", body: \"body\") }\r\n\r\n    expect(post).to be_a(ReadOnlyPost)\r\n  end\r\nend\r\n```\r\n\r\nThe model would be writable only inside the block of `with_writable`, and outside the normal readonly value would hold true. Specifying the model that we want to override as the argument allows us to be intentional about which model we're overriding rather than blanket making everything writable (which could lead to some unexpected consequences).\r\n\r\nRuby's malleable nature allows us to make this override a reality with relative ease. We can open up the class to modification with [`class_eval`](https://docs.ruby-lang.org/en/4.0/Module.html#method-i-class_eval) and override `readonly?`, execute the block, and then revert `readonly?` back to it's original value.\r\n\r\n``` ruby\r\n# spec/support/readonly_helper.rb\r\ndef with_writable(klass)\r\n  klass.class_eval do\r\n    alias_method :_original_readonly?, :readonly?\r\n    define_method(:readonly?) { false }\r\n  end\r\n  yield\r\nensure\r\n  klass.class_eval do\r\n    alias_method :readonly?, :_original_readonly?\r\n    remove_method :_original_readonly?\r\n  end\r\nend\r\n\r\n```\r\n\r\nWith this definition living in `spec/support`, it can be used in any test, but makes it inconvenient to accidentally use in app code (nothing is impossible in ruby - you could still require it in app code, but doing so would be a code smell and hopefully convince you that's not the way to go).\r\n\r\nYou could extend this helper to accept an array of classes and override `readonly?` for each if you had multiple readonly models for which you needed to create data.\r\n\r\n## Other Options\r\n\r\nI think the above solution provides the clearest intent and doesn't change the model's behavior during test execution. There are other ways to implement this but I think the drawbacks make them less desirable.\r\n\r\n### As an RSpec Example Group Helper\r\n\r\n``` ruby\r\ndescribe \"example group\", writable: ReadOnlyPost do\r\n  # ...\r\nend\r\n```\r\n\r\nYou could make a `writable` helper to add to RSpec example groups or individual tests. This is arguably a cleaner interface than my preferred solution, but I couldn't find a way to make the override apply only during test setup, not during execution. That makes this a no go for me, since I lose confidence the test will behave the same way the application does in production - meaning I can't rely on this test.\r\n\r\n### Write SQL Directly\r\n\r\n```ruby\r\nRSpec.describe ReadOnlyPost, type: :model do\r\n  before do\r\n    ActiveRecord::Base.connection.execute(\"INSERT INTO read_only_posts VALUES ('Title', 'Body'\")\r\n  end\r\n\r\n  it \"can create a post\" do\r\n    expect(ReadOnlyPost.last).to be_a(ReadOnlyPost)\r\n  end\r\nend\r\n```\r\n\r\nYou could write SQL statements directly to insert/update records. But this bypasses all of the ORM features of ActiveRecord and really feels like fighting Rails rather than extending it.\r\n\r\n## Conclusion \r\n\r\nSo there's a way to test readonly models effectively. Hope it's helpful, and let me know if you use this or a different solution yourself!\r\n\r\nPhoto by [John Cardamone](https://unsplash.com/@jocallen) on [Unsplash](https://unsplash.com/photos/a-close-up-of-a-lock-on-a-red-and-white-door-Evr4B9JTa94)","content_text":"I was working with a readonly model in Rails the other day and ran into an issue whilst testing it. Here's what I ran into and the solution I came up with.\n\nReadonly models are a great way to signal that, well, you should only ever read them, not write them. Maybe you have some external system that connects to the database for writes, or maybe your Rails app connects to some data warehouse for some queries or reports. It's can be useful to have a safeguard to prevent accidental errant writes. It's actually really simple to make a model readonly, you just need to override the readonly? method:\nclass ReadOnlyPost \u0026lt; ApplicationRecord\n  def readonly? = true\nend\n\nNow any attempt to create/save/update/delete a ReadOnlyPost will raise a friendly ActiveRecord::ReadOnlyRecord exception.\nThe Problem\n\nYou might be able to see where this is going. \n\nFor any tests that can avoid saving this readonly model to the database, (i.e. using ReadOnlyPost.new or FactoryBot.build), then we're all good. But often tests need to persist some records to the test database. And if I try to create a ReadOnlyPost, I'm going to have a bad time.\nRSpec.describe ReadOnlyPost, type: :model do\n  let(:post) { ReadOnlyPost.create(title: \"Title\", body: \"body\") }\n\n  it \"can create a post\" do\n    expect(post).to be_a(ReadOnlyPost)\n  end\nend\n% bundle exec rspec\nF\n\nFailures:\n\n  1) ReadOnlyPost can create a post\n     Failure/Error: let(:post) { ReadOnlyPost.create(title: \"Title\", body: \"body\") }\n\n     ActiveRecord::ReadOnlyRecord:\n       ReadOnlyPost is marked as readonly\n     # ./spec/models/read_only_post_spec.rb:4:in `block (2 levels) in \u0026lt;top (required)\u0026gt;'\n     # ./spec/models/read_only_post_spec.rb:7:in `block (2 levels) in \u0026lt;top (required)\u0026gt;'\n\nThis makes sense, the readonly property of the model doesn't go away in test - creating a record is creating a record is writing to the database, so it will fail.\nThe Solution\n\nWhat we want to do is override this constraint to temporarily allow us to persist data. Ideally, we do this very narrowly to not impact the system under test. If the model is writable during test execution, then the behavior in the test is different from production behavior and that can lead to bugs in production that we cannot capture in a test. So ideally we can override this during test setup only and revert back to readonly by the time the test executes.\n\nGiven those constraints, a nice API would look like this:\nRSpec.describe ReadOnlyPost, type: :model do\n  # In a let\n  let(:post) { with_writable(ReadOnlyPost) { ReadOnlyPost.create(title: \"Title\", body: \"body\") } }\n\n  # Or in a before block\n  before do\n    with_writable(ReadOnlyPost) do\n      ReadOnlyPost.create(title: \"Title\", body: \"body\")\n    end\n  end\n\n  it \"can create a post\" do\n    # Even inline in a test\n    with_writable(ReadOnlyPost) { ReadOnlyPost.create(title: \"Title\", body: \"body\") }\n\n    expect(post).to be_a(ReadOnlyPost)\n  end\nend\n\nThe model would be writable only inside the block of with_writable, and outside the normal readonly value would hold true. Specifying the model that we want to override as the argument allows us to be intentional about which model we're overriding rather than blanket making everything writable (which could lead to some unexpected consequences).\n\nRuby's malleable nature allows us to make this override a reality with relative ease. We can open up the class to modification with class_eval and override readonly?, execute the block, and then revert readonly? back to it's original value.\n# spec/support/readonly_helper.rb\ndef with_writable(klass)\n  klass.class_eval do\n    alias_method :_original_readonly?, :readonly?\n    define_method(:readonly?) { false }\n  end\n  yield\nensure\n  klass.class_eval do\n    alias_method :readonly?, :_original_readonly?\n    remove_method :_original_readonly?\n  end\nend\n\n\nWith this definition living in spec/support, it can be used in any test, but makes it inconvenient to accidentally use in app code (nothing is impossible in ruby - you could still require it in app code, but doing so would be a code smell and hopefully convince you that's not the way to go).\n\nYou could extend this helper to accept an array of classes and override readonly? for each if you had multiple readonly models for which you needed to create data.\nOther Options\n\nI think the above solution provides the clearest intent and doesn't change the model's behavior during test execution. There are other ways to implement this but I think the drawbacks make them less desirable.\nAs an RSpec Example Group Helper\ndescribe \"example group\", writable: ReadOnlyPost do\n  # ...\nend\n\nYou could make a writable helper to add to RSpec example groups or individual tests. This is arguably a cleaner interface than my preferred solution, but I couldn't find a way to make the override apply only during test setup, not during execution. That makes this a no go for me, since I lose confidence the test will behave the same way the application does in production - meaning I can't rely on this test.\nWrite SQL Directly\nRSpec.describe ReadOnlyPost, type: :model do\n  before do\n    ActiveRecord::Base.connection.execute(\"INSERT INTO read_only_posts VALUES ('Title', 'Body'\")\n  end\n\n  it \"can create a post\" do\n    expect(ReadOnlyPost.last).to be_a(ReadOnlyPost)\n  end\nend\n\nYou could write SQL statements directly to insert/update records. But this bypasses all of the ORM features of ActiveRecord and really feels like fighting Rails rather than extending it.\nConclusion\n\nSo there's a way to test readonly models effectively. Hope it's helpful, and let me know if you use this or a different solution yourself!\n\nPhoto by John Cardamone on Unsplash\n","summary":"I was working with a readonly model in Rails the other day and ran into an issue whilst testing it. Here's what I ran into and the solution I came up with.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1167/john-cardamone-Evr4B9JTa94-unsplash.jpg","date_published":"2026-03-17T09:00:00-04:00","data_modified":"2026-03-14T10:07:48-04:00","author":{"name":"Tony Yunker","url":"https://hashrocket.com/team/tony-yunker","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/110/IMG_5394.jpeg"},"tags":["Testing","Ruby on Rails"]},{"id":"https://hashrocket.com/blog/posts/crafting-code-building-a-ruby-pattern-generator-for-a-crochet-circle","url":"https://hashrocket.com/blog/posts/crafting-code-building-a-ruby-pattern-generator-for-a-crochet-circle","title":"Crafting Code: Building a Ruby Pattern Generator for a Crochet Circle","content_html":"In my time as a developer, I have noticed that one of the most common ways my coworkers spend time coding outside of work is by developing little code snippets or apps that solve problems in their everyday lives. From household budgeting, to managing workouts on rowing machines, to generating a Taco Bell order, these projects allow devs to explore different coding styles and learn new technologies.\r\n\r\nFor a long time, most of my side projects have been for the sole purpose of learning a new technology. When I wanted to start building mobile apps with React Native, I wrote a small to-do app that, once finished, I abandoned. The same thing happened when I wanted to try to use PostgreSQL's listen and notify feature to build a live updating chat app. So, when I was thinking about a new side project, I decided it was time to work on something that could be long lived and help me with one of my favorite hobbies: crocheting.\n\n# The Premise\r\n\r\nRecently, I've been making a lot of small projects that have started with a base shape that then gets built upon. Often, this shape is a circle. After running through several projects, I started to notice a pattern of increases and repetitions for each row. It occurred to me that if the shape followed a specific pattern, I could probably build a ruby class to generate that pattern. Thus began this side project!\r\n\r\n## Breaking Down a Simplified Pattern\r\n\r\nTo begin, we have to inspect the pattern. Crochet patterns follow a specific format, and use abbreviations for the types of stitches being used.\r\n\r\nThe example pattern uses the following abbreviations and rules:\r\n\r\n| Abbreviation | Meaning | Use | Stitch Count |\r\n|:---:|---|---|:---:|\r\n| **sc** | single crochet | adds a stitch to the round | 1 |\r\n| **inc** | increase _(two single crochets in the same stitch)_ | adds an extra stitch to the round | 2 |\r\n\r\nWith those abbreviations in mind, we can start parsing the pattern.\r\n\r\n```\r\nR1: 6sc in magic ring (6)\r\nR2: [inc] x6 (12)\r\nR3: [sc, inc] x6 (18)\r\nR4: sc, inc, [2sc, inc] x5, sc (24)\r\nR5: [3sc, inc] x6 (30)\r\nR6: 2sc, inc, [4sc, inc] x5, 2sc (36)\r\nR7: [5sc, inc] x6 (42)\r\nR8: 3sc, inc, [6sc, inc] x5, 3sc (48)\r\n```\r\n\r\nWithout knowing the details of how crochet patterns work, we can still get an idea of the format here. First, we can see that each line starts with `R{n}:`. This is the indicator of the round we are working on. We can see here that the pattern ends on `R8`, so we know that we're going to have exactly 8 rounds to the circle.\r\n\r\nSecond, we can see that each line ends with a number in parentheses. This number represents the final stitch count for the round. Here is where we may begin to see the beginnings of a pattern. Looking at the stitch counts, we see that we're starting with 6 stitches, and then increasing by 6 in each round. Given that behavior, the equation for determining the stitch count based on a given row would be `6 + 6(R - 1)`, where `R` is the row number.\r\n\r\nIn between the round number and stitch count are the actual directions for the round. In some cases, such as round 3, there are square brackets around a stitch or set of stitches, followed by `x{n}`. This is an indicator that the stitch or stitches within the brackets are going to be repeated `n` times. So in round 4, where the directions are `sc, inc, [2sc, inc] x5, sc`, what we're actually doing is:\r\n\r\n```\r\nsc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc\r\n```\r\n\r\n## Starting the Ruby Class\r\n\r\nNow that the stitch count equation has been determined, a small ruby class can start to be built up. The class will expect an input of how many rows, and output an array of strings in the correct format for each row. Also, given that we have an example pattern, an rspec test expecting the correct output can be written.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  attr_reader :row_number\r\n\r\n  def self.generate(row_count:)\r\n    (1..row_count).map do |row_number|\r\n      new(row_number:).pattern\r\n    end\r\n  end\r\n\r\n  def initialize(row_number:)\r\n    @row_number = row_number\r\n  end\r\n\r\n  def pattern\r\n    \"#{row_title}: #{instructions} (#{stitch_count})\"\r\n  end\r\n\r\n  private\r\n\r\n  def row_title\r\n    \"R#{row_number}\"\r\n  end\r\n\r\n  def stitch_count\r\n    6 + 6 * (row_number - 1)\r\n  end\r\n\r\n  def instructions\r\n  end\r\nend\r\n\r\nRSpec.describe CirclePatternGenerator do\r\n  describe \".generate\" do\r\n    let(:expected_output) do\r\n      [\r\n        \"R1: 6sc in magic ring (6)\",\r\n        \"R2: [inc] x6 (12)\",\r\n        \"R3: [sc, inc] x6 (18)\",\r\n        \"R4: sc, inc, [2sc, inc] x5, sc (24)\",\r\n        \"R5: [3sc, inc] x6 (30)\",\r\n        \"R6: 2sc, inc, [4sc, inc] x5, 2sc (36)\",\r\n        \"R7: [5sc, inc] x6 (42)\",\r\n        \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\",\r\n      ]\r\n    end\r\n\r\n    it \"outputs the correct pattern\" do\r\n      expect(described_class.generate(row_count: 8)).to eq(expected_output)\r\n    end\r\n  end\r\nend\r\n```\r\n\r\nRunning the specs, we can confirm that the row titles and stitch counts are correct, and the instructions are the last thing to figure out.\r\n\r\n```shell\r\n$ rspec circle_pattern_generator.rb\r\n\r\nRandomized with seed 47053\r\nF\r\n\r\nFailures:\r\n\r\n  1) CirclePatternGenerator.generate outputs the correct pattern\r\n     Failure/Error:\r\n       expect(described_class.generate(row_count: 8)).to eq(expected_output)\r\n\r\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\r\n            got: [\"R1:  (6)\", \"R2:  (12)\", \"R3:  (18)\", \"R4:  (24)\", \"R5:  (30)\", \"R6:  (36)\", \"R7:  (42)\", \"R8:  (48)\"]\r\n```\r\n\r\n## Examining the Instructions\r\n\r\nThe core instructions for each row will be the most involved part of this pattern generator. Knowing that each row is made up of single crochets and increases, we can break down the instructions for the rows into the totals for each kind of stitch:\r\n\r\n| Row Number | Instructions | Single Crochets | Increases |\r\n| :-: | - | :-: | :-: |\r\n| 1 | 6sc in magic ring | 6 | 0 |\r\n| 2 | [inc] x6 | 0 | 6 |\r\n| 3 | [sc, inc] x6 | 6 | 6 |\r\n| 4 | sc, inc, [2sc, inc] x5, sc | 12 | 6 |\r\n| 5 | [3sc, inc] x6 | 18 | 6 |\r\n| 6 | 2sc, inc, [4sc, inc] x5, 2sc | 24 | 6 |\r\n| 7 | [5sc, inc] x6 | 30 | 6 |\r\n| 8 | 3sc, inc, [6sc, inc] x5, 3sc | 36 | 6 |\r\n\r\nLooking at the table, we can see that the number of increases is 6 starting from the second row, and doesn't change as the rows progress. It's also apparent that the number of single crochets increases by 6 per row after starting from 0 on the second row. The first row, therefore, appears to be an outlier to the pattern evident from lines 2 - 8. The first row is also the only row that has special instructions (\"in magic ring\").\r\n\r\nIf we treat the first row like a special case, we can add a guard clause to the `instructions` method on the ruby class.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  # ...\r\n\r\n  def instructions\r\n    return first_row_instructions if first_row?\r\n  end\r\n\r\n  def first_row_instructions\r\n    \"6sc in magic ring\"\r\n  end\r\n\r\n  def first_row?\r\n    row_number == 1\r\n  end\r\nend\r\n```\r\n\r\nRunning the specs again, we can see that the first row now matches the expected output.\r\n\r\n```shell\r\n$ rspec circle_pattern_generator.rb\r\n\r\nRandomized with seed 47053\r\nF\r\n\r\nFailures:\r\n\r\n  1) CirclePatternGenerator.generate outputs the correct pattern\r\n     Failure/Error:\r\n       expect(described_class.generate(row_count: 8)).to eq(expected_output)\r\n\r\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\r\n            got: [\"R1: 6sc in magic ring (6)\", \"R2:  (12)\", \"R3:  (18)\", \"R4:  (24)\", \"R5:  (30)\", \"R6:  (36)\", \"R7:  (42)\", \"R8:  (48)\"]\r\n```\r\n\r\nWith the special case out of the way, it's time to look into the rows that adhere to the pattern of single crochets and increases.\r\n\r\nThe most noticeable thing in the row instructions is the number of single crochets that are used to space out the increases. In row 2, there are none, but from there, we can see that row 3 has 1 single crochet between increases, followed by 2 in row 4, 3 in row 5, and so on. We have ourselves another small equation -- the number of single crochets spacing out the increases is `R - 2`, where `R` is our row number. We can add that as a method to use for the instructions in the ruby class.\r\n\r\n```ruby\r\ndef sc_count\r\n  row_number - 2\r\nend\r\n```\r\n\r\n Returing to the pattern, it appears that the only difference in the way the instructions work is that in the odd numbered rows, we repeat the same thing 6 times, and in the even numbered rows, the repeat is only 5 times. The reason the repeat on the even rows is 5 times is because the single crochet spacing appears to be split between the beginning and end of the row. We again have a bit of an outlier with the second row, since it doesn't have any single crochets spacing out the increases.\r\n\r\n```shell\r\n# no sc\r\nR2: [inc] x6 (12)\r\n\r\n# sc in the beginning and end, totaling 2sc\r\nR4: sc, inc, [2sc, inc] x5, sc (24)\r\n\r\n# 2sc in the beginning and end, totaling 4sc\r\nR6: 2sc, inc, [4sc, inc] x5, 2sc (36)\r\n\r\n# 3sc in the beginning and end, totaling 6sc\r\nR8: 3sc, inc, [6sc, inc] x5, 3sc (48)\r\n```\r\n\r\nSince we have another outlier, it seems easiest to add a second guard clause to the class.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  # ...\r\n\r\n  def instructions\r\n    return first_row_instructions if first_row?\r\n    return second_row_instructions if second_row?\r\n  end\r\n\r\n  # ...\r\n\r\n  def second_row_instructions\r\n    \"[inc] x6\"\r\n  end\r\n\r\n  def second_row?\r\n    row_number == 2\r\n  end\r\nend\r\n```\r\n\r\nNow, we can build out the logic for the even and odd rows, starting with the easier option: the odd rows.\r\n\r\n## Programming the Odd Rows\r\n\r\nUsing the `sc_count` method determined earlier, the odd numbered rows should follow the pattern of `\"[#{sc_count}sc, inc] x6\"`.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  # ...\r\n\r\n  def instructions\r\n    return first_row_instructions if first_row?\r\n    return second_row_instructions if second_row?\r\n\r\n    if row_number.odd?\r\n      odd_row_instructions\r\n    end\r\n  end\r\n\r\n  def odd_row_instructions\r\n    \"[#{sc_count}sc, inc] x6\"\r\n  end\r\n\r\n  # ...\r\nend\r\n```\r\n\r\nRunning the tests again, we see that this _almost_ looks right, except that there shouldn't be a `1` in front of the `sc` for row 3 (`\"R3: [1sc, inc] x6 (18)\"`).\r\n\r\n```shell\r\n$ rspec circle_pattern_generator.rb\r\n\r\nRandomized with seed 37350\r\nF\r\n\r\nFailures:\r\n\r\n  1) CirclePatternGenerator.generate outputs the correct pattern\r\n     Failure/Error: expect(described_class.generate(row_count: 8)).to eq(expected_output)\r\n\r\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\r\n            got: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [1sc, inc] x6 (18)\", \"R4:  (24)\", \"R5: [3sc, inc] x6 (30)\", \"R6:  (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8:  (48)\"]\r\n```\r\n\r\nWhile we can fix this for row 3, this isn't the only case where there's just one single crochet. Perhaps it would be worth creating a method to handle the display of the single crochets.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  # ...\r\n\r\n  def odd_row_instructions\r\n    \"[#{sc_pattern(sc_count)}, inc] x6\"\r\n  end\r\n\r\n  def sc_pattern(number)\r\n    (number == 1) ? \"sc\" : \"#{number}sc\"\r\n  end\r\n\r\n  # ...\r\nend\r\n```\r\n\r\nRerunning the tests, we can see that this solves the issue for row 3.\r\n\r\n```shell\r\n$ rspec circle_pattern_generator.rb\r\n\r\nRandomized with seed 45147\r\nF\r\n\r\nFailures:\r\n\r\n  1) CirclePatternGenerator.generate outputs the correct pattern\r\n     Failure/Error: expect(described_class.generate(row_count: 8)).to eq(expected_output)\r\n\r\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\r\n            got: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4:  (24)\", \"R5: [3sc, inc] x6 (30)\", \"R6:  (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8:  (48)\"]\r\n```\r\n\r\nNow we can address the even rows.\r\n\r\n## Programming the Even Rows\r\n\r\nAs noted before, the even rows repeat 5 times, and have the 6th repetition split across the beginning and end of the row.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  # ...\r\n\r\n  def instructions\r\n    return first_row_instructions if first_row?\r\n    return second_row_instructions if second_row?\r\n\r\n    row_number.odd? ? odd_row_instructions : even_row_instructions\r\n  end\r\n\r\n  def even_row_instructions\r\n    split_pattern = sc_pattern(sc_count / 2)\r\n    \"#{split_pattern}, inc, [#{sc_pattern(sc_count)}, inc] x5, #{split_pattern}\"\r\n  end\r\n\r\n  # ...\r\nend\r\n```\r\n\r\nRunning the test again, we have green! We have created a pattern generator for crochet circles based on a specified row count.\r\n\r\n```ruby\r\nclass CirclePatternGenerator\r\n  attr_reader :row_number\r\n\r\n  def self.generate(row_count:)\r\n    (1..row_count).map do |row_number|\r\n      new(row_number:).pattern\r\n    end\r\n  end\r\n\r\n  def initialize(row_number:)\r\n    @row_number = row_number\r\n  end\r\n\r\n  def pattern\r\n    \"#{row_title}: #{instructions} (#{stitch_count})\"\r\n  end\r\n\r\n  private\r\n\r\n  def row_title\r\n    \"R#{row_number}\"\r\n  end\r\n\r\n  def stitch_count\r\n    6 + 6 * (row_number - 1)\r\n  end\r\n\r\n  def instructions\r\n    return first_row_instructions if first_row?\r\n    return second_row_instructions if second_row?\r\n\r\n    row_number.odd? ? odd_row_instructions : even_row_instructions\r\n  end\r\n\r\n  def odd_row_instructions\r\n    \"[#{sc_pattern(sc_count)}, inc] x6\"\r\n  end\r\n\r\n  def even_row_instructions\r\n    split_pattern = sc_pattern(sc_count / 2)\r\n    \"#{split_pattern}, inc, [#{sc_pattern(sc_count)}, inc] x5, #{split_pattern}\"\r\n  end\r\n\r\n  def sc_pattern(number)\r\n    (number == 1) ? \"sc\" : \"#{number}sc\"\r\n  end\r\n\r\n  def sc_count\r\n    row_number - 2\r\n  end\r\n\r\n  def first_row_instructions\r\n    \"6sc in magic ring\"\r\n  end\r\n\r\n  def first_row?\r\n    row_number == 1\r\n  end\r\n\r\n  def second_row_instructions\r\n    \"[inc] x6\"\r\n  end\r\n\r\n  def second_row?\r\n    row_number == 2\r\n  end\r\nend\r\n```\r\n\r\n## Takeaways\r\n\r\nThis was a fun little experiment, but not entirely useful on its own. However, what if 3D shapes in crochet also follow a similar pattern? Would I be able to generate spheres? Or cubes? Would a row count really be the ideal entry point for such shapes? It seems that more research and experimentation are needed!","content_text":"In my time as a developer, I have noticed that one of the most common ways my coworkers spend time coding outside of work is by developing little code snippets or apps that solve problems in their everyday lives. From household budgeting, to managing workouts on rowing machines, to generating a Taco Bell order, these projects allow devs to explore different coding styles and learn new technologies.\n\nFor a long time, most of my side projects have been for the sole purpose of learning a new technology. When I wanted to start building mobile apps with React Native, I wrote a small to-do app that, once finished, I abandoned. The same thing happened when I wanted to try to use PostgreSQL's listen and notify feature to build a live updating chat app. So, when I was thinking about a new side project, I decided it was time to work on something that could be long lived and help me with one of my favorite hobbies: crocheting.\nThe Premise\n\nRecently, I've been making a lot of small projects that have started with a base shape that then gets built upon. Often, this shape is a circle. After running through several projects, I started to notice a pattern of increases and repetitions for each row. It occurred to me that if the shape followed a specific pattern, I could probably build a ruby class to generate that pattern. Thus began this side project!\nBreaking Down a Simplified Pattern\n\nTo begin, we have to inspect the pattern. Crochet patterns follow a specific format, and use abbreviations for the types of stitches being used.\n\nThe example pattern uses the following abbreviations and rules:\n\n\n\nAbbreviation\nMeaning\nUse\nStitch Count\n\n\n\nsc\nsingle crochet\nadds a stitch to the round\n1\n\n\ninc\nincrease (two single crochets in the same stitch)\nadds an extra stitch to the round\n2\n\n\n\nWith those abbreviations in mind, we can start parsing the pattern.\nR1: 6sc in magic ring (6)\nR2: [inc] x6 (12)\nR3: [sc, inc] x6 (18)\nR4: sc, inc, [2sc, inc] x5, sc (24)\nR5: [3sc, inc] x6 (30)\nR6: 2sc, inc, [4sc, inc] x5, 2sc (36)\nR7: [5sc, inc] x6 (42)\nR8: 3sc, inc, [6sc, inc] x5, 3sc (48)\n\nWithout knowing the details of how crochet patterns work, we can still get an idea of the format here. First, we can see that each line starts with R{n}:. This is the indicator of the round we are working on. We can see here that the pattern ends on R8, so we know that we're going to have exactly 8 rounds to the circle.\n\nSecond, we can see that each line ends with a number in parentheses. This number represents the final stitch count for the round. Here is where we may begin to see the beginnings of a pattern. Looking at the stitch counts, we see that we're starting with 6 stitches, and then increasing by 6 in each round. Given that behavior, the equation for determining the stitch count based on a given row would be 6 + 6(R - 1), where R is the row number.\n\nIn between the round number and stitch count are the actual directions for the round. In some cases, such as round 3, there are square brackets around a stitch or set of stitches, followed by x{n}. This is an indicator that the stitch or stitches within the brackets are going to be repeated n times. So in round 4, where the directions are sc, inc, [2sc, inc] x5, sc, what we're actually doing is:\nsc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc, sc, inc, sc\nStarting the Ruby Class\n\nNow that the stitch count equation has been determined, a small ruby class can start to be built up. The class will expect an input of how many rows, and output an array of strings in the correct format for each row. Also, given that we have an example pattern, an rspec test expecting the correct output can be written.\nclass CirclePatternGenerator\n  attr_reader :row_number\n\n  def self.generate(row_count:)\n    (1..row_count).map do |row_number|\n      new(row_number:).pattern\n    end\n  end\n\n  def initialize(row_number:)\n    @row_number = row_number\n  end\n\n  def pattern\n    \"#{row_title}: #{instructions} (#{stitch_count})\"\n  end\n\n  private\n\n  def row_title\n    \"R#{row_number}\"\n  end\n\n  def stitch_count\n    6 + 6 * (row_number - 1)\n  end\n\n  def instructions\n  end\nend\n\nRSpec.describe CirclePatternGenerator do\n  describe \".generate\" do\n    let(:expected_output) do\n      [\n        \"R1: 6sc in magic ring (6)\",\n        \"R2: [inc] x6 (12)\",\n        \"R3: [sc, inc] x6 (18)\",\n        \"R4: sc, inc, [2sc, inc] x5, sc (24)\",\n        \"R5: [3sc, inc] x6 (30)\",\n        \"R6: 2sc, inc, [4sc, inc] x5, 2sc (36)\",\n        \"R7: [5sc, inc] x6 (42)\",\n        \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\",\n      ]\n    end\n\n    it \"outputs the correct pattern\" do\n      expect(described_class.generate(row_count: 8)).to eq(expected_output)\n    end\n  end\nend\n\nRunning the specs, we can confirm that the row titles and stitch counts are correct, and the instructions are the last thing to figure out.\n$ rspec circle_pattern_generator.rb\n\nRandomized with seed 47053\nF\n\nFailures:\n\n  1) CirclePatternGenerator.generate outputs the correct pattern\n     Failure/Error:\n       expect(described_class.generate(row_count: 8)).to eq(expected_output)\n\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\n            got: [\"R1:  (6)\", \"R2:  (12)\", \"R3:  (18)\", \"R4:  (24)\", \"R5:  (30)\", \"R6:  (36)\", \"R7:  (42)\", \"R8:  (48)\"]\nExamining the Instructions\n\nThe core instructions for each row will be the most involved part of this pattern generator. Knowing that each row is made up of single crochets and increases, we can break down the instructions for the rows into the totals for each kind of stitch:\n\n\n\nRow Number\nInstructions\nSingle Crochets\nIncreases\n\n\n\n1\n6sc in magic ring\n6\n0\n\n\n2\n[inc] x6\n0\n6\n\n\n3\n[sc, inc] x6\n6\n6\n\n\n4\nsc, inc, [2sc, inc] x5, sc\n12\n6\n\n\n5\n[3sc, inc] x6\n18\n6\n\n\n6\n2sc, inc, [4sc, inc] x5, 2sc\n24\n6\n\n\n7\n[5sc, inc] x6\n30\n6\n\n\n8\n3sc, inc, [6sc, inc] x5, 3sc\n36\n6\n\n\n\nLooking at the table, we can see that the number of increases is 6 starting from the second row, and doesn't change as the rows progress. It's also apparent that the number of single crochets increases by 6 per row after starting from 0 on the second row. The first row, therefore, appears to be an outlier to the pattern evident from lines 2 - 8. The first row is also the only row that has special instructions (\"in magic ring\").\n\nIf we treat the first row like a special case, we can add a guard clause to the instructions method on the ruby class.\nclass CirclePatternGenerator\n  # ...\n\n  def instructions\n    return first_row_instructions if first_row?\n  end\n\n  def first_row_instructions\n    \"6sc in magic ring\"\n  end\n\n  def first_row?\n    row_number == 1\n  end\nend\n\nRunning the specs again, we can see that the first row now matches the expected output.\n$ rspec circle_pattern_generator.rb\n\nRandomized with seed 47053\nF\n\nFailures:\n\n  1) CirclePatternGenerator.generate outputs the correct pattern\n     Failure/Error:\n       expect(described_class.generate(row_count: 8)).to eq(expected_output)\n\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\n            got: [\"R1: 6sc in magic ring (6)\", \"R2:  (12)\", \"R3:  (18)\", \"R4:  (24)\", \"R5:  (30)\", \"R6:  (36)\", \"R7:  (42)\", \"R8:  (48)\"]\n\nWith the special case out of the way, it's time to look into the rows that adhere to the pattern of single crochets and increases.\n\nThe most noticeable thing in the row instructions is the number of single crochets that are used to space out the increases. In row 2, there are none, but from there, we can see that row 3 has 1 single crochet between increases, followed by 2 in row 4, 3 in row 5, and so on. We have ourselves another small equation -- the number of single crochets spacing out the increases is R - 2, where R is our row number. We can add that as a method to use for the instructions in the ruby class.\ndef sc_count\n  row_number - 2\nend\n\nReturing to the pattern, it appears that the only difference in the way the instructions work is that in the odd numbered rows, we repeat the same thing 6 times, and in the even numbered rows, the repeat is only 5 times. The reason the repeat on the even rows is 5 times is because the single crochet spacing appears to be split between the beginning and end of the row. We again have a bit of an outlier with the second row, since it doesn't have any single crochets spacing out the increases.\n# no sc\nR2: [inc] x6 (12)\n\n# sc in the beginning and end, totaling 2sc\nR4: sc, inc, [2sc, inc] x5, sc (24)\n\n# 2sc in the beginning and end, totaling 4sc\nR6: 2sc, inc, [4sc, inc] x5, 2sc (36)\n\n# 3sc in the beginning and end, totaling 6sc\nR8: 3sc, inc, [6sc, inc] x5, 3sc (48)\n\nSince we have another outlier, it seems easiest to add a second guard clause to the class.\nclass CirclePatternGenerator\n  # ...\n\n  def instructions\n    return first_row_instructions if first_row?\n    return second_row_instructions if second_row?\n  end\n\n  # ...\n\n  def second_row_instructions\n    \"[inc] x6\"\n  end\n\n  def second_row?\n    row_number == 2\n  end\nend\n\nNow, we can build out the logic for the even and odd rows, starting with the easier option: the odd rows.\nProgramming the Odd Rows\n\nUsing the sc_count method determined earlier, the odd numbered rows should follow the pattern of \"[#{sc_count}sc, inc] x6\".\nclass CirclePatternGenerator\n  # ...\n\n  def instructions\n    return first_row_instructions if first_row?\n    return second_row_instructions if second_row?\n\n    if row_number.odd?\n      odd_row_instructions\n    end\n  end\n\n  def odd_row_instructions\n    \"[#{sc_count}sc, inc] x6\"\n  end\n\n  # ...\nend\n\nRunning the tests again, we see that this almost looks right, except that there shouldn't be a 1 in front of the sc for row 3 (\"R3: [1sc, inc] x6 (18)\").\n$ rspec circle_pattern_generator.rb\n\nRandomized with seed 37350\nF\n\nFailures:\n\n  1) CirclePatternGenerator.generate outputs the correct pattern\n     Failure/Error: expect(described_class.generate(row_count: 8)).to eq(expected_output)\n\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\n            got: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [1sc, inc] x6 (18)\", \"R4:  (24)\", \"R5: [3sc, inc] x6 (30)\", \"R6:  (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8:  (48)\"]\n\nWhile we can fix this for row 3, this isn't the only case where there's just one single crochet. Perhaps it would be worth creating a method to handle the display of the single crochets.\nclass CirclePatternGenerator\n  # ...\n\n  def odd_row_instructions\n    \"[#{sc_pattern(sc_count)}, inc] x6\"\n  end\n\n  def sc_pattern(number)\n    (number == 1) ? \"sc\" : \"#{number}sc\"\n  end\n\n  # ...\nend\n\nRerunning the tests, we can see that this solves the issue for row 3.\n$ rspec circle_pattern_generator.rb\n\nRandomized with seed 45147\nF\n\nFailures:\n\n  1) CirclePatternGenerator.generate outputs the correct pattern\n     Failure/Error: expect(described_class.generate(row_count: 8)).to eq(expected_output)\n\n       expected: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4: sc, inc, [2sc, inc] ...c, inc, [4sc, inc] x5, 2sc (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8: 3sc, inc, [6sc, inc] x5, 3sc (48)\"]\n            got: [\"R1: 6sc in magic ring (6)\", \"R2: [inc] x6 (12)\", \"R3: [sc, inc] x6 (18)\", \"R4:  (24)\", \"R5: [3sc, inc] x6 (30)\", \"R6:  (36)\", \"R7: [5sc, inc] x6 (42)\", \"R8:  (48)\"]\n\nNow we can address the even rows.\nProgramming the Even Rows\n\nAs noted before, the even rows repeat 5 times, and have the 6th repetition split across the beginning and end of the row.\nclass CirclePatternGenerator\n  # ...\n\n  def instructions\n    return first_row_instructions if first_row?\n    return second_row_instructions if second_row?\n\n    row_number.odd? ? odd_row_instructions : even_row_instructions\n  end\n\n  def even_row_instructions\n    split_pattern = sc_pattern(sc_count / 2)\n    \"#{split_pattern}, inc, [#{sc_pattern(sc_count)}, inc] x5, #{split_pattern}\"\n  end\n\n  # ...\nend\n\nRunning the test again, we have green! We have created a pattern generator for crochet circles based on a specified row count.\nclass CirclePatternGenerator\n  attr_reader :row_number\n\n  def self.generate(row_count:)\n    (1..row_count).map do |row_number|\n      new(row_number:).pattern\n    end\n  end\n\n  def initialize(row_number:)\n    @row_number = row_number\n  end\n\n  def pattern\n    \"#{row_title}: #{instructions} (#{stitch_count})\"\n  end\n\n  private\n\n  def row_title\n    \"R#{row_number}\"\n  end\n\n  def stitch_count\n    6 + 6 * (row_number - 1)\n  end\n\n  def instructions\n    return first_row_instructions if first_row?\n    return second_row_instructions if second_row?\n\n    row_number.odd? ? odd_row_instructions : even_row_instructions\n  end\n\n  def odd_row_instructions\n    \"[#{sc_pattern(sc_count)}, inc] x6\"\n  end\n\n  def even_row_instructions\n    split_pattern = sc_pattern(sc_count / 2)\n    \"#{split_pattern}, inc, [#{sc_pattern(sc_count)}, inc] x5, #{split_pattern}\"\n  end\n\n  def sc_pattern(number)\n    (number == 1) ? \"sc\" : \"#{number}sc\"\n  end\n\n  def sc_count\n    row_number - 2\n  end\n\n  def first_row_instructions\n    \"6sc in magic ring\"\n  end\n\n  def first_row?\n    row_number == 1\n  end\n\n  def second_row_instructions\n    \"[inc] x6\"\n  end\n\n  def second_row?\n    row_number == 2\n  end\nend\nTakeaways\n\nThis was a fun little experiment, but not entirely useful on its own. However, what if 3D shapes in crochet also follow a similar pattern? Would I be able to generate spheres? Or cubes? Would a row count really be the ideal entry point for such shapes? It seems that more research and experimentation are needed!\n","summary":"In my time as a developer, I have noticed that one of the most common ways my coworkers spend time coding outside of work is by developing little code snippets or apps that solve problems in their everyday lives. From household budgeting, to managing workouts on rowing machines, to generating a Taco Bell order, these projects allow devs to explore different coding styles and learn new technologies.\n\nFor a long time, most of my side projects have been for the sole purpose of learning a new technology. When I wanted to start building mobile apps with React Native, I wrote a small to-do app that, once finished, I abandoned. The same thing happened when I wanted to try to use PostgreSQL's listen and notify feature to build a live updating chat app. So, when I was thinking about a new side project, I decided it was time to work on something that could be long lived and help me with one of my favorite hobbies: crocheting.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1134/jan-antonin-kolar-cMtaW0Jk5Ew-unsplash.jpg","date_published":"2026-01-20T09:00:00-05:00","data_modified":"2026-01-20T08:56:07-05:00","author":{"name":"Mary Lee","url":"https://hashrocket.com/team/mary-lee","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/100/mary_lee.jpg"},"tags":["Ruby"]},{"id":"https://hashrocket.com/blog/posts/building-a-simple-search-with-rails-stimulus","url":"https://hashrocket.com/blog/posts/building-a-simple-search-with-rails-stimulus","title":"Building a (Very) Simple Responsive Search with Rails \u0026 Stimulus","content_html":"Here's a simple and responsive search form I put together for a recent side project, using Hotwire's Stimulus framework and Rails with Turbo.\n\n## The Form\r\n\r\nHere's how the search input looks. It's a simple search form connected to a Stimulus controller. The input triggers the search function upon every input event. The debounce logic occurs in the Stimulus controller to make sure not too many requests are made to the server:\r\n\r\n```erb\r\n\u003c%= form_with url: items_path, method: :get,\r\n    data: { controller: \"search\", turbo_frame: \"items_list\" } do |f| %\u003e\r\n  \u003c%= f.text_field :query,\r\n      placeholder: \"Search...\",\r\n      data: {\r\n        search_target: \"input\",\r\n        action: \"input-\u003esearch#search\"\r\n      } %\u003e\r\n  \u003cbutton type=\"button\"\r\n      class=\"hidden\"\r\n      data-search-target=\"clearButton\"\r\n      data-action=\"click-\u003esearch#clear\"\u003e\r\n    ×\r\n  \u003c/button\u003e\r\n\u003c% end %\u003e\r\n```\r\n\r\nThere's an additional feature here: a button which appears in the search field whenever the field has a value. The button has a data-action attribute pointing to the clear action. If the button is clicked, the input value is cleared out.\r\n\r\n## The Stimulus Controller\r\n\r\nAll we need to do here is submit the form with a debounce timer, handle the action for clicking the clear input button, and handle hiding the clear button if the input field has a value or not.\r\n\r\n```javascript\r\nexport default class extends Controller {\r\n  static targets = [\"input\", \"clearButton\"]\r\n\r\n  connect() {\r\n    this.toggleClear()\r\n  }\r\n\r\n  search() {\r\n    this.toggleClear()\r\n    clearTimeout(this.timeout)\r\n\r\n    this.timeout = setTimeout(() =\u003e {\r\n      this.element.requestSubmit()\r\n    }, 300)\r\n  }\r\n\r\n  clear() {\r\n    this.inputTarget.value = \"\"\r\n    this.toggleClear()\r\n    this.element.requestSubmit()\r\n  }\r\n\r\n  toggleClear() {\r\n    if (this.inputTarget.value.length \u003e 0) {\r\n      this.clearButtonTarget.classList.remove(\"hidden\")\r\n    } else {\r\n      this.clearButtonTarget.classList.add(\"hidden\")\r\n    }\r\n  }\r\n}\r\n\r\n```\r\n\r\n## The Turbo Frame\r\n\r\nYou'll notice the form targets an `items_list` turbo frame, which contains your search results in the view. When the Stimulus controller submits the form,  the content inside this frame gets replaced with the new search results. This is a pretty simple way to update the search results without full page reloads. I also wanted to avoid turbo streams for this implementation, because I find them a tad awkward sometimes.\r\n\r\n```erb\r\n\u003c%= turbo_frame_tag \"items_list\" do %\u003e\r\n  \u003c% @items.each do |item| %\u003e\r\n    \u003c%= render item %\u003e\r\n  \u003c% end %\u003e\r\n\u003c% end %\u003e\r\n```\r\n\r\n## The Rails Controller\r\n\r\nStandard index action with optional query filtering. Works with or without search params:\r\n\r\n```ruby\r\ndef index\r\n  # you will probably paginate your results\r\n  if params[:query].present?\r\n    @items = Item.where(\"name LIKE ?\", \"%#{params[:query]}%\")\r\n  else\r\n    @items = Item.all\r\n  end\r\nend\r\n```\r\n\r\n## Conclusion\r\nThis is a nice simple starting point to build out some more complicated search features from. I'd recommend using a pagination gem like [pagy](https://github.com/ddnexus/pagy) to handle the search results while keep your view manageable.","content_text":"Here's a simple and responsive search form I put together for a recent side project, using Hotwire's Stimulus framework and Rails with Turbo.\nThe Form\n\nHere's how the search input looks. It's a simple search form connected to a Stimulus controller. The input triggers the search function upon every input event. The debounce logic occurs in the Stimulus controller to make sure not too many requests are made to the server:\n\u0026lt;%= form_with url: items_path, method: :get,\n    data: { controller: \"search\", turbo_frame: \"items_list\" } do |f| %\u0026gt;\n  \u0026lt;%= f.text_field :query,\n      placeholder: \"Search...\",\n      data: {\n        search_target: \"input\",\n        action: \"input-\u0026gt;search#search\"\n      } %\u0026gt;\n  \u0026lt;button type=\"button\"\n      class=\"hidden\"\n      data-search-target=\"clearButton\"\n      data-action=\"click-\u0026gt;search#clear\"\u0026gt;\n    ×\n  \u0026lt;/button\u0026gt;\n\u0026lt;% end %\u0026gt;\n\nThere's an additional feature here: a button which appears in the search field whenever the field has a value. The button has a data-action attribute pointing to the clear action. If the button is clicked, the input value is cleared out.\nThe Stimulus Controller\n\nAll we need to do here is submit the form with a debounce timer, handle the action for clicking the clear input button, and handle hiding the clear button if the input field has a value or not.\nexport default class extends Controller {\n  static targets = [\"input\", \"clearButton\"]\n\n  connect() {\n    this.toggleClear()\n  }\n\n  search() {\n    this.toggleClear()\n    clearTimeout(this.timeout)\n\n    this.timeout = setTimeout(() =\u0026gt; {\n      this.element.requestSubmit()\n    }, 300)\n  }\n\n  clear() {\n    this.inputTarget.value = \"\"\n    this.toggleClear()\n    this.element.requestSubmit()\n  }\n\n  toggleClear() {\n    if (this.inputTarget.value.length \u0026gt; 0) {\n      this.clearButtonTarget.classList.remove(\"hidden\")\n    } else {\n      this.clearButtonTarget.classList.add(\"hidden\")\n    }\n  }\n}\n\nThe Turbo Frame\n\nYou'll notice the form targets an items_list turbo frame, which contains your search results in the view. When the Stimulus controller submits the form,  the content inside this frame gets replaced with the new search results. This is a pretty simple way to update the search results without full page reloads. I also wanted to avoid turbo streams for this implementation, because I find them a tad awkward sometimes.\n\u0026lt;%= turbo_frame_tag \"items_list\" do %\u0026gt;\n  \u0026lt;% @items.each do |item| %\u0026gt;\n    \u0026lt;%= render item %\u0026gt;\n  \u0026lt;% end %\u0026gt;\n\u0026lt;% end %\u0026gt;\nThe Rails Controller\n\nStandard index action with optional query filtering. Works with or without search params:\ndef index\n  # you will probably paginate your results\n  if params[:query].present?\n    @items = Item.where(\"name LIKE ?\", \"%#{params[:query]}%\")\n  else\n    @items = Item.all\n  end\nend\nConclusion\n\nThis is a nice simple starting point to build out some more complicated search features from. I'd recommend using a pagination gem like pagy to handle the search results while keep your view manageable.\n","summary":"Here's a simple and responsive search form I put together for a recent side project, using Hotwire's Stimulus framework and Rails with Turbo.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1101/Screenshot_2025-12-09_at_10.39.16_AM.png","date_published":"2025-12-09T09:00:00-05:00","data_modified":"2025-12-09T10:39:39-05:00","author":{"name":"Jack Rosa","url":"https://hashrocket.com/team/jackrosa","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/112/headshot.jpg"},"tags":["rails","Hotwire","Turbo","Stimulus"]},{"id":"https://hashrocket.com/blog/posts/creating-a-custom-mobile-integration-for-a-board-game-using-ruby-on-rails","url":"https://hashrocket.com/blog/posts/creating-a-custom-mobile-integration-for-a-board-game-using-ruby-on-rails","title":"Creating a Custom Mobile Integration for a Board Game Using Ruby on Rails","content_html":"Picture this: you find a charming old board game at a garage sale, bring it home, gather some friends—and snap. The 50-year-old plastic components break instantly. You search the web for help to replace this fun and unique game mechanic but there’s nothing to be found. So naturally, you roll up your sleeves and build your own mobile version. No one else does this? Just me? Well, in case you ever find yourself in a similar boat, I figured I would walk you through what I did when building my own mobile integration to the 1973 Parker Brothers classic [Billionare](https://boardgamegeek.com/boardgame/1436/billionaire).\n\nAs I said, right when I went to try out this cool \"new\" board game for the first time, the plastic ends of the `Analyzer` snapped. The analyzer is basically a plastic rod with 2  floating spinners on them. The spinners, when rotated, will either land with a red face or a green face. You then refer to the chart to see what action to take. I thought this was such a unique way to give a random effect in a game, without relying on dice. It essentially is just a random binary spinner that counts up to 4. Here is what it looks like for reference:\r\n\r\n![image](https://i.imgur.com/rKJADkM.png)\r\n\r\nSo, without any 4 sided die lying around, and not wanting to use a boring random number generator, my brain went right to what it knows best---Ruby on Rails.\r\n\r\nSo without further ado, here's what I did and how you can do something similar yourself.\r\n\r\n# Creating the app\r\n\r\nSure, Rails could be considered overkill for such a simple app. But my idea is to allow this project to scale into a whole library of board game helpers that can all live within the same app. So, to start off I ran the old trusty `rails new` command, with a couple preferences I like to pass in to make my life easier as a developer.\r\n\r\n```ruby\r\nrails new board_game_library --css tailwind --database=postgresql\r\n```\r\n\r\nOne of the main tools I leveraged for this project besides Rails was [Tailwind](https://www.tailwindcss.com). Tailwind makes styling so easy. Here at Hashrocket Tailwind is pretty standard for all of us, so if you're interested in any tips or tricks, we have plenty of [blog posts](https://hashrocket.com/blog/search?utf8=%E2%9C%93\u0026search_term=tailwind) and [TILs](https://til.hashrocket.com/?q=tailwind) worth checking out!\r\n\r\nThe app itself is very simple. Here is everything I needed to do:\r\n\r\n- Create a view to host the Analyzer\r\n- Create a stimulus controller to \"scramble\" the Analyzer\r\n- Expose `ngrok` as a host in the config so I could access the Analyzer from my phone for free\r\n\r\n# The View\r\n\r\nThe view is nothing revolutionary. I just wanted a simple interface that gave me all the capabilities of the Analyzer from the board game. So, I needed 2 \"spinners\", a way to spin them, and a chart that let you know what action to take.\r\n\r\n## Spinners\r\nHere is the html for the spinners. Simply 2 boxes side by side that have some `data-analyzer-target`s that are used by the stimulus controller. I also give them some default colors here to give the user an idea of what to expect when the app opens up.\r\n\r\n```html\r\n\u003cdiv class=\"flex space-x-4\"\u003e\r\n  \u003cdiv class=\"bg-red-400 w-1/2 h-24\" data-analyzer-target=\"left\"\u003e\r\n  \u003c/div\u003e\r\n\r\n  \u003cdiv class=\"bg-green-400 w-1/2 h-24\" data-analyzer-target=\"right\"\u003e\r\n  \u003c/div\u003e\r\n\u003c/div\u003e\r\n```\r\n\r\n## Spinner button\r\nNext we have the call to action, the \"spinner\" button. This is simply a nicely styled button that will call the `randomize` method on the stimulus controller.\r\n\r\n```html\r\n\u003cdiv class=\"w-fit mx-auto mt-12\"\u003e\r\n  \u003cbutton class=\"border border-black rounded-md shadow-lg p-2\" data-action=\"click-\u003eanalyzer#randomize\" data-analyzer-target=\"analyzer\"\u003e\r\n    ANALYZE\r\n  \u003c/button\u003e\r\n\u003c/div\u003e\r\n```\r\n\r\n## Actions Chart\r\nThe actions chart is just simply a bunch of elements of text, styled with html, along with a visual representation of the matching result. I'll give an example of one, but to save screen space on this post, I won't post the whole thing. Remember, you can always check out the source code [here](https://github.com/vanillaHafer/billionare-analyzer/tree/main).\r\n\r\n```html\r\n\u003cdiv class=\"mt-12\"\u003e\r\n  \u003cdiv class=\"w-fit mx-auto border border-black rounded-lg p-4 space-y-4\"\u003e\r\n    \u003cdiv class=\"flex space-x-6\"\u003e\r\n      \u003cdiv class=\"flex space-x-1 my-auto\"\u003e\r\n        \u003cdiv class=\"w-8 h-4 bg-red-400 border border-black\"\u003e\u003c/div\u003e\r\n        \u003cdiv class=\"w-8 h-4 bg-red-400 border border-black\"\u003e\u003c/div\u003e\r\n      \u003c/div\u003e\r\n\r\n      \u003cdiv\u003e\r\n        Take investment you are analyzing.\r\n      \u003c/div\u003e\r\n    \u003c/div\u003e\r\n\r\n    \u003c!-- Other divs for the other outcomes --\u003e\r\n\u003c/div\u003e\r\n```\r\n\r\n# The Controller\r\n\r\nFor my stimulus controller I gave it the targets you saw in the examples above, as well as some functions to help us randomize. Here is a high-level view of a few of the methods. If you want to see the code for yourself, please check out the [repo here](https://github.com/vanillaHafer/billionare-analyzer/tree/main):\r\n\r\n- `randomize()` **Called via the Analyze button. Kicks off the randomization, or *spinning***\r\n- `disableButton()` **Disables the Analyze button**\r\n- `randomAnimation()` **Starts animating the *left* and *right* spinners on the page**\r\n- `makeDecision()` **Determine via 50% chance for each spinner whether it will be red or green**\r\n- `enableButton()` **Enable the Analyze button when everything is finished**\r\n\r\n# Accessing from my phone\r\n\r\nFor the final piece of the puzzle, I wanted a way to easily view this from my phone so I didn't need to bring a big laptop or something to the table. So, for a simple and easy way to host this \"on the web\" without actually paying for a domain and setting all that up, I used the free version of [ngrok](https://ngrok.com/). Now, for this to work properly with your Rails app, you will need to add one line to your `config/application.rb` file.\r\n\r\n```ruby\r\n#config/application.rb\r\n\r\n# ...\r\nconfig.hosts \u003c\u003c \".ngrok-free.app\"\r\n# ...\r\n```\r\n\r\nThis allows any hosts that end with `.ngrok-free.app` to have access to the application. This way you can start and stop ngrok as much as you wish, and you don't need to manually input the random host in every time.\r\n\r\nNow simply start your rails server, start up ngrok, and access it from your phone or any other device and you should have something like this as your final product:\r\n\r\n\u003cimg src=\"https://i.imgur.com/9MZ63Vf.gif\" width=\"250px\"\u003e\r\n# Conclusion\r\n\r\nSo, this ended up being a fun way to play the game with some friends. It worked exactly as expected and it was also really cool to be playing a 50 year old board game that had its own custom mobile integration, even if it was small and simple.\r\n\r\nDon't want to go through all the work to make your own Rails app? Feel free to reach out to Hashrocket for your next project! Big or small, we'd love to hear about how we can help you make the app you want to get the results you need!\r\n\r\n\u003chr /\u003e\r\n      \r\nCover Photo by [Thomas Buchhol](https://unsplash.com/@markusspiske) on [Unsplash](https://unsplash.com/photos/a-close-up-of-a-bunch-of-buttons-on-a-table-0n7_eiAQZwA?utm_source=unsplash\u0026utm_medium=referral\u0026utm_content=creditCopyText\"). \r\n\r\nAnalyzer image provided by [Thomas Melinsky](https://boardgamegeek.com/profile/mithras) on [BoardGameGeek](https://boardgamegeek.com/image/776001/billionaire).","content_text":"Picture this: you find a charming old board game at a garage sale, bring it home, gather some friends—and snap. The 50-year-old plastic components break instantly. You search the web for help to replace this fun and unique game mechanic but there’s nothing to be found. So naturally, you roll up your sleeves and build your own mobile version. No one else does this? Just me? Well, in case you ever find yourself in a similar boat, I figured I would walk you through what I did when building my own mobile integration to the 1973 Parker Brothers classic Billionare.\n\nAs I said, right when I went to try out this cool \"new\" board game for the first time, the plastic ends of the Analyzer snapped. The analyzer is basically a plastic rod with 2  floating spinners on them. The spinners, when rotated, will either land with a red face or a green face. You then refer to the chart to see what action to take. I thought this was such a unique way to give a random effect in a game, without relying on dice. It essentially is just a random binary spinner that counts up to 4. Here is what it looks like for reference:\n\n\n\nSo, without any 4 sided die lying around, and not wanting to use a boring random number generator, my brain went right to what it knows best---Ruby on Rails.\n\nSo without further ado, here's what I did and how you can do something similar yourself.\nCreating the app\n\nSure, Rails could be considered overkill for such a simple app. But my idea is to allow this project to scale into a whole library of board game helpers that can all live within the same app. So, to start off I ran the old trusty rails new command, with a couple preferences I like to pass in to make my life easier as a developer.\nrails new board_game_library --css tailwind --database=postgresql\n\nOne of the main tools I leveraged for this project besides Rails was Tailwind. Tailwind makes styling so easy. Here at Hashrocket Tailwind is pretty standard for all of us, so if you're interested in any tips or tricks, we have plenty of blog posts and TILs worth checking out!\n\nThe app itself is very simple. Here is everything I needed to do:\n\n\nCreate a view to host the Analyzer\nCreate a stimulus controller to \"scramble\" the Analyzer\nExpose ngrok as a host in the config so I could access the Analyzer from my phone for free\n\nThe View\n\nThe view is nothing revolutionary. I just wanted a simple interface that gave me all the capabilities of the Analyzer from the board game. So, I needed 2 \"spinners\", a way to spin them, and a chart that let you know what action to take.\nSpinners\n\nHere is the html for the spinners. Simply 2 boxes side by side that have some data-analyzer-targets that are used by the stimulus controller. I also give them some default colors here to give the user an idea of what to expect when the app opens up.\n\u0026lt;div class=\"flex space-x-4\"\u0026gt;\n  \u0026lt;div class=\"bg-red-400 w-1/2 h-24\" data-analyzer-target=\"left\"\u0026gt;\n  \u0026lt;/div\u0026gt;\n\n  \u0026lt;div class=\"bg-green-400 w-1/2 h-24\" data-analyzer-target=\"right\"\u0026gt;\n  \u0026lt;/div\u0026gt;\n\u0026lt;/div\u0026gt;\nSpinner button\n\nNext we have the call to action, the \"spinner\" button. This is simply a nicely styled button that will call the randomize method on the stimulus controller.\n\u0026lt;div class=\"w-fit mx-auto mt-12\"\u0026gt;\n  \u0026lt;button class=\"border border-black rounded-md shadow-lg p-2\" data-action=\"click-\u0026gt;analyzer#randomize\" data-analyzer-target=\"analyzer\"\u0026gt;\n    ANALYZE\n  \u0026lt;/button\u0026gt;\n\u0026lt;/div\u0026gt;\nActions Chart\n\nThe actions chart is just simply a bunch of elements of text, styled with html, along with a visual representation of the matching result. I'll give an example of one, but to save screen space on this post, I won't post the whole thing. Remember, you can always check out the source code here.\n\u0026lt;div class=\"mt-12\"\u0026gt;\n  \u0026lt;div class=\"w-fit mx-auto border border-black rounded-lg p-4 space-y-4\"\u0026gt;\n    \u0026lt;div class=\"flex space-x-6\"\u0026gt;\n      \u0026lt;div class=\"flex space-x-1 my-auto\"\u0026gt;\n        \u0026lt;div class=\"w-8 h-4 bg-red-400 border border-black\"\u0026gt;\u0026lt;/div\u0026gt;\n        \u0026lt;div class=\"w-8 h-4 bg-red-400 border border-black\"\u0026gt;\u0026lt;/div\u0026gt;\n      \u0026lt;/div\u0026gt;\n\n      \u0026lt;div\u0026gt;\n        Take investment you are analyzing.\n      \u0026lt;/div\u0026gt;\n    \u0026lt;/div\u0026gt;\n\n    \u0026lt;!-- Other divs for the other outcomes --\u0026gt;\n\u0026lt;/div\u0026gt;\nThe Controller\n\nFor my stimulus controller I gave it the targets you saw in the examples above, as well as some functions to help us randomize. Here is a high-level view of a few of the methods. If you want to see the code for yourself, please check out the repo here:\n\n\nrandomize() Called via the Analyze button. Kicks off the randomization, or *spinning*\ndisableButton() Disables the Analyze button\nrandomAnimation() Starts animating the left and right spinners on the page\nmakeDecision() Determine via 50% chance for each spinner whether it will be red or green\nenableButton() Enable the Analyze button when everything is finished\n\nAccessing from my phone\n\nFor the final piece of the puzzle, I wanted a way to easily view this from my phone so I didn't need to bring a big laptop or something to the table. So, for a simple and easy way to host this \"on the web\" without actually paying for a domain and setting all that up, I used the free version of ngrok. Now, for this to work properly with your Rails app, you will need to add one line to your config/application.rb file.\n#config/application.rb\n\n# ...\nconfig.hosts \u0026lt;\u0026lt; \".ngrok-free.app\"\n# ...\n\nThis allows any hosts that end with .ngrok-free.app to have access to the application. This way you can start and stop ngrok as much as you wish, and you don't need to manually input the random host in every time.\n\nNow simply start your rails server, start up ngrok, and access it from your phone or any other device and you should have something like this as your final product:\n\n\nConclusion\n\nSo, this ended up being a fun way to play the game with some friends. It worked exactly as expected and it was also really cool to be playing a 50 year old board game that had its own custom mobile integration, even if it was small and simple.\n\nDon't want to go through all the work to make your own Rails app? Feel free to reach out to Hashrocket for your next project! Big or small, we'd love to hear about how we can help you make the app you want to get the results you need!\n\n\n\nCover Photo by Thomas Buchhol on Unsplash. \n\nAnalyzer image provided by Thomas Melinsky on BoardGameGeek.\n","summary":"Picture this: you find a charming old board game at a garage sale, bring it home, gather some friends—and snap. The 50-year-old plastic components break instantly. You search the web for help to replace this fun and unique game mechanic but there’s nothing to be found. So naturally, you roll up your sleeves and build your own mobile version. No one else does this? Just me? Well, in case you ever find yourself in a similar boat, I figured I would walk you through what I did when building my own mobile integration to the 1973 Parker Brothers classic Billionare.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1073/bg.jpg","date_published":"2025-12-04T09:00:00-05:00","data_modified":"2025-11-14T11:53:48-05:00","author":{"name":"Craig Hafer","url":"https://hashrocket.com/team/craig-hafer","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/108/IMG_4001.jpg"},"tags":["Ruby","Mobile","rails","board game app"]},{"id":"https://hashrocket.com/blog/posts/why-ruby-is-the-best-language-for-advent-of-code","url":"https://hashrocket.com/blog/posts/why-ruby-is-the-best-language-for-advent-of-code","title":"Why Ruby is the Best Language for Advent of Code","content_html":"It's the most wonderful time of the year - ~~Christmas~~ Advent of Code time! [Advent of Code](https://adventofcode.com/) is an Advent Calendar style series of programming puzzles put out each year, starting on December 1st leading up to Christmas. The puzzles are super festive and ramp up in difficulty over the course of the month. Programmers of every level can participate, and in researching some of the more difficult problems you'll probably learn something cool! It's a great way to finish out the year.\n\nI've been taking part in Advent of Code since 2019 (I've never completed a full year - and that's ok! You can participate for as long as it's fun and have the time) and have tried solving in multiple different languages - Advent is a great way to learn/skill up in a new language. But Ruby remains my favorite language in which to solve these puzzles.\r\n\r\nMany of Ruby's strengths - its flexibility, robust standard library, and tooling make it the ideal language for Advent of Code.\r\n\r\n## Flexibility\r\n\r\nRuby doesn't enforce any one way of writing code. Want to solve a problem with a procedural script? Go for it! Want to leverage object-oriented programming and send messages between classes? Can do! Want to write in a functional style and `map` and `zip` a data structure in one long chain? You can do that too! And you can mix and match paradigms between problems - whatever models each problem best.\r\n\r\nRuby's data structures are super flexible as well. In many of the problems, `Array` and `Hash` allow you to very quickly model solutions. But if you find a hash isn't quite cutting it and you don't want to upgrade it to a full class, you can use the [`Data`](https://docs.ruby-lang.org/en/master/Data.html) class to create value objects. This will lend you a bit more structure than a hash, and allow you to encapsulate some logic inside it without having to bring in the overhead of a `Class`. \r\n\r\nSome of Ruby's flexibility - metaprogramming in particular - can be...unpopular... in larger production codebases. It's the \"magic\" that can lead to some nasty bugs. But Advent is a great place to play with those features. So go ahead, open up `String` and redefine some methods! Live life dangerously!\r\n\r\n## Standard Library\r\n\r\nThe Ruby standard library is well-suited for Advent of Code. \r\n\r\n`Array` and `Hash` will probably be your best friends, and can get you pretty far in Advent. Because they both implement [`Enumerable`](https://docs.ruby-lang.org/en/master/Enumerable.html), you can easily slice, dice, and transform these data structures with higher order functions like `map`, `reduce`, `zip`, and `each`. Convenience methods like `sum` and `chunk` mean you don't have to reinvent the wheel over and over again.\r\n\r\nSet operations are built in - union (`|`), intersection (`\u0026`), difference (`-`), and XOR (`^`). I've had to define these in other languages and while not difficult, it takes away from the actual solving of the puzzle.\r\n\r\nThe built in [`matrix`](https://github.com/ruby/matrix) gem can be a nicer way to represent grids than using arrays of arrays. It also includes some utilities to `transpose`, `inverse` and check if `symmetric` or upper or lower triangular. \r\n\r\nThe [`prime`](https://github.com/ruby/prime) gem (included in Ruby) has utilities for listing prime numbers, determining if a number is prime, and decomposing integers into their prime factorization. It's up to you if you want to write this kind of thing yourself or skip ahead to the more exciting parts of the puzzle.\r\n\r\n## Tooling\r\n\r\nRuby's tooling makes it very easy to run, debug, and test your Advent solutions. Drop into an [`irb`](https://ruby.github.io/irb/) session, load in your code and play around with it. Within the REPL you can run the whole solution, or poke around at different methods to see if they're behaving as expected. Use [`debug`](https://github.com/ruby/debug) or [`pry`](https://github.com/pry/pry) to pause execution and check the current state of your variables. When you're exploring solutions and not quite sure where to go or what's going wrong, a REPL is a great way to think, try things out, and iterate.\r\n\r\nYou can also write tests for Advent of Code! (I do!) RSpec or Minitest are easy to add into a project. You can write unit tests for various helper methods. And you can write tests for each solution's output - great if you want to refactor or try out a performance optimization and ensure you still get the same solution.\r\n\r\n## Summing it up\r\n \r\nSure, other languages have some or all of these features. But I don't know of any other language that has them all and is as pleasant to use as in Ruby.\r\n\r\nRuby was designed for programmer happiness. So wouldn't that make it the best choice for a fun programming challenge? The two were made for each other.\r\n\r\nPhoto by [Jocelyn Allen](https://unsplash.com/@jocallen) on [Unsplash](https://unsplash.com/photos/a-snowy-street-lined-with-wooden-buildings-N6B1DVpP36Q)","content_text":"It's the most wonderful time of the year - Christmas Advent of Code time! Advent of Code is an Advent Calendar style series of programming puzzles put out each year, starting on December 1st leading up to Christmas. The puzzles are super festive and ramp up in difficulty over the course of the month. Programmers of every level can participate, and in researching some of the more difficult problems you'll probably learn something cool! It's a great way to finish out the year.\n\nI've been taking part in Advent of Code since 2019 (I've never completed a full year - and that's ok! You can participate for as long as it's fun and have the time) and have tried solving in multiple different languages - Advent is a great way to learn/skill up in a new language. But Ruby remains my favorite language in which to solve these puzzles.\n\nMany of Ruby's strengths - its flexibility, robust standard library, and tooling make it the ideal language for Advent of Code.\nFlexibility\n\nRuby doesn't enforce any one way of writing code. Want to solve a problem with a procedural script? Go for it! Want to leverage object-oriented programming and send messages between classes? Can do! Want to write in a functional style and map and zip a data structure in one long chain? You can do that too! And you can mix and match paradigms between problems - whatever models each problem best.\n\nRuby's data structures are super flexible as well. In many of the problems, Array and Hash allow you to very quickly model solutions. But if you find a hash isn't quite cutting it and you don't want to upgrade it to a full class, you can use the Data class to create value objects. This will lend you a bit more structure than a hash, and allow you to encapsulate some logic inside it without having to bring in the overhead of a Class. \n\nSome of Ruby's flexibility - metaprogramming in particular - can be...unpopular... in larger production codebases. It's the \"magic\" that can lead to some nasty bugs. But Advent is a great place to play with those features. So go ahead, open up String and redefine some methods! Live life dangerously!\nStandard Library\n\nThe Ruby standard library is well-suited for Advent of Code. \n\nArray and Hash will probably be your best friends, and can get you pretty far in Advent. Because they both implement Enumerable, you can easily slice, dice, and transform these data structures with higher order functions like map, reduce, zip, and each. Convenience methods like sum and chunk mean you don't have to reinvent the wheel over and over again.\n\nSet operations are built in - union (|), intersection (\u0026amp;), difference (-), and XOR (^). I've had to define these in other languages and while not difficult, it takes away from the actual solving of the puzzle.\n\nThe built in matrix gem can be a nicer way to represent grids than using arrays of arrays. It also includes some utilities to transpose, inverse and check if symmetric or upper or lower triangular. \n\nThe prime gem (included in Ruby) has utilities for listing prime numbers, determining if a number is prime, and decomposing integers into their prime factorization. It's up to you if you want to write this kind of thing yourself or skip ahead to the more exciting parts of the puzzle.\nTooling\n\nRuby's tooling makes it very easy to run, debug, and test your Advent solutions. Drop into an irb session, load in your code and play around with it. Within the REPL you can run the whole solution, or poke around at different methods to see if they're behaving as expected. Use debug or pry to pause execution and check the current state of your variables. When you're exploring solutions and not quite sure where to go or what's going wrong, a REPL is a great way to think, try things out, and iterate.\n\nYou can also write tests for Advent of Code! (I do!) RSpec or Minitest are easy to add into a project. You can write unit tests for various helper methods. And you can write tests for each solution's output - great if you want to refactor or try out a performance optimization and ensure you still get the same solution.\nSumming it up\n\nSure, other languages have some or all of these features. But I don't know of any other language that has them all and is as pleasant to use as in Ruby.\n\nRuby was designed for programmer happiness. So wouldn't that make it the best choice for a fun programming challenge? The two were made for each other.\n\nPhoto by Jocelyn Allen on Unsplash\n","summary":"It's the most wonderful time of the year - Christmas Advent of Code time! Advent of Code is an Advent Calendar style series of programming puzzles put out each year, starting on December 1st leading up to Christmas. The puzzles are super festive and ramp up in difficulty over the course of the month. Programmers of every level can participate, and in researching some of the more difficult problems you'll probably learn something cool! It's a great way to finish out the year.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1072/jocelyn-allen-N6B1DVpP36Q-unsplash.jpg","date_published":"2025-12-02T09:00:00-05:00","data_modified":"2025-12-01T09:56:38-05:00","author":{"name":"Tony Yunker","url":"https://hashrocket.com/team/tony-yunker","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/110/IMG_5394.jpeg"},"tags":["Ruby"]},{"id":"https://hashrocket.com/blog/posts/how-to-rev-up-your-rails-development-with-mcp","url":"https://hashrocket.com/blog/posts/how-to-rev-up-your-rails-development-with-mcp","title":"How To Rev Up Your Rails Development with MCP","content_html":"Shipping new features on legacy Rails applications requires deep codebase context. The rails-mcp-server gem closes the gap between AI agents and your Rails projects, enabling more relevant code analysis and context aware refactoring suggestions. Whether you're dealing with tech debt in a brownfield application or building new greenfield features, this tool can help you move faster with confidence.\n\nThe Model Context Protocol (MCP) is a way to allow LLM models to interact with development environments and external tools. The rails-mcp-server gem is a Ruby implementation that enables LLMs to interact directly with Rails projects through MCP; Once you have it set up with an agent like claude or copilot, the model will have way more context about your app's architecture and removes a lot of the nonsense and guesswork associated with AI driven development. Check out the [repo here](https://github.com/maquina-app/rails-mcp-server)\r\n\r\nI'll walk you through setting up the rails-mcp-server gem for your Rails projects.\r\n\r\n## Installation\r\n\r\nInstalling the rails-mcp-server gem is simple like any ruby gem. Open your terminal and run. \r\n\r\nDon't install it to your project's directory or add it to a Rails gemfile, this gem is meant to be installed globally and configured to run with multiple projects.\r\n\r\n```bash\r\ngem install rails-mcp-server\r\n```\r\n\r\n## Config\r\n\r\n### Setting Up Your Projects\r\n\r\nOnce you run the server for the first time, you can configure the gem to access your rails projects. The configuration location depends on your operating system:\r\n\r\n- **macOS**: `$XDG_CONFIG_HOME/rails-mcp` or `~/.config/rails-mcp` if `XDG_CONFIG_HOME` is not set\r\n- **Windows**: `%APPDATA%\\rails-mcp`\r\n\r\nThe first time the server runs, these directories will be created.\r\n\r\n## Running the Rails MCP Server\r\n\r\nThe server can be ran in two modes, but for the purposes of this article we will stick to http mode, if you want to find out about STDIO mode check out the [docs here](https://github.com/maquina-app/rails-mcp-server). Running the server will create the config directory.\r\n\r\n### HTTP Mode\r\n\r\nHTTP mode runs as an HTTP server with JSON-RPC and Server-Sent Events (SSE) endpoints, perfect for web applications. Lets start it up.\r\n\r\n```bash\r\n# Start on the default port (6029)\r\nrails-mcp-server --mode http\r\n\r\n# Starting on a custom port\r\nrails-mcp-server --mode http -p 8080\r\n```\r\n\r\nWhen running in HTTP mode, the server can be accessed at these endpoints:\r\n\r\n- **JSON-RPC endpoint**: `http://localhost:\u003cport\u003e/mcp/messages`\r\n- **SSE endpoint**: `http://localhost:\u003cport\u003e/mcp/sse`\r\n\r\n### Configuring Your Rails Projects\r\n\r\nThe server will also create a  `projects.yml` file in your config directory when you run it; to include your Rails projects, just provide a project name and a path to the directory:\r\n\r\n```yaml\r\n# ~/.config/rails-mcp/projects.yml\r\ntest_app: \"~/projects/test_app\"\r\n```\r\n\r\n\r\n## Integrating with Claude Code\r\n\r\nAdd the following to your ```claude/config.json```:\r\n\r\n```json\r\n{\r\n  \"mcpServers\": {\r\n    \"railsMcpServer\": {\r\n      \"command\": \"ruby\",\r\n      \"args\": [\"/full/path/to/rails-mcp-server/exe/rails-mcp-server\"]\r\n    }\r\n  }\r\n}\r\n```\r\n\r\nTo find the full path to your rails-mcp-server executable:\r\n\r\n```bash\r\nwhich rails-mcp-server\r\n```\r\n\r\nRestart the claude code session to refresh the new config. \r\n\r\nWith a new claude code session started, use the command ```/mcp``` to see that the session has access to the rails-mcp-server.\r\n\r\n## Integrating with Copilot\r\n\r\nAdd the following to your ```.vscode/mcp.json```:\r\n\r\n```json\r\n{\r\n  \"servers\": {\r\n    \"railsMcpServer\": {\r\n      \"command\": \"ruby\",\r\n      \"args\": [\"/full/path/to/rails-mcp-server/exe/rails-mcp-server\"]\r\n    }\r\n  }\r\n}\r\n```\r\n\r\nTo find the full path to your rails-mcp-server executable:\r\n\r\n```bash\r\nwhich rails-mcp-server\r\n```\r\n\r\n## Using The Rails MCP Server\r\n\r\nOnce configured, your AI assistant can interact with your Rails projects using the provided tools, check out the docs too see the [provided tools](https://github.com/maquina-app/rails-mcp-server).\r\n\r\n### Analyzing Your Code\r\n\r\nA helpful thing to note is that you dont need to use the tools names specifically in LLM chats. You can simply refrence them in plain english. \r\n\r\n```\r\nload the Turbo guides and then show me how to refactor my blog feed with turbo streams\r\n```\r\n\r\nBefore you can use the MCP tools on your project, you will have to tell the mcp to switch to your project. *it will have to be a project name that is included in ```projects.yml```*\r\n\r\nHere's some of the tools can use in your LLM chats and how you might use them in a prompt:\r\n\r\n```\r\nproject_info\r\n\r\n=\u003e break down each of the 3rd party integrations used in the project\r\n```\r\n\r\n\r\n```\r\nget_routes\r\n\r\n=\u003e which routes are being used for the messaging endpoints?\r\n```\r\n\r\n```\r\nanalyze_models\r\n\r\n=\u003e how is the user model associated with the blogpost model?\r\n\r\n```\r\n\r\nSee how the prompt above says ```load the turbo guides```, the LLM is smart enough to know that it will use the ```load_guide``` tool to respond to the prompt.\r\n\r\n\r\n## Provide Even More Context\r\n\r\nWhile the MCP server gives your AI assistants access to your code structure, providing more context about what you're working on helps generate better suggestions. Be sure to mention:\r\n\r\n- The specific files you want to work with\r\n- A clear description of the feature or bug you're addressing\r\n- Any architectural or logistical constraints (obviously)\r\n\r\n## Conclusion\r\n\r\nThe rails-mcp-server gem fills in the context gap between AI and your Rails development workflow.\r\n\r\nWhether you're working on a single application or switching between multiple Rails projects, the MCP server provides a way for AI, and hopefully you, to understand your codebase better.\r\n\r\n## Extras\r\n\r\n- [GitHub Repository](https://github.com/maquina-app/rails-mcp-server)\r\n- [MCP Specification](https://modelcontextprotocol.io)\r\n\r\nReach out to Hashrocket if you need help modernizing your Rails Project! 🚀","content_text":"Shipping new features on legacy Rails applications requires deep codebase context. The rails-mcp-server gem closes the gap between AI agents and your Rails projects, enabling more relevant code analysis and context aware refactoring suggestions. Whether you're dealing with tech debt in a brownfield application or building new greenfield features, this tool can help you move faster with confidence.\n\nThe Model Context Protocol (MCP) is a way to allow LLM models to interact with development environments and external tools. The rails-mcp-server gem is a Ruby implementation that enables LLMs to interact directly with Rails projects through MCP; Once you have it set up with an agent like claude or copilot, the model will have way more context about your app's architecture and removes a lot of the nonsense and guesswork associated with AI driven development. Check out the repo here\n\nI'll walk you through setting up the rails-mcp-server gem for your Rails projects.\nInstallation\n\nInstalling the rails-mcp-server gem is simple like any ruby gem. Open your terminal and run. \n\nDon't install it to your project's directory or add it to a Rails gemfile, this gem is meant to be installed globally and configured to run with multiple projects.\ngem install rails-mcp-server\nConfig\nSetting Up Your Projects\n\nOnce you run the server for the first time, you can configure the gem to access your rails projects. The configuration location depends on your operating system:\n\n\nmacOS: $XDG_CONFIG_HOME/rails-mcp or ~/.config/rails-mcp if XDG_CONFIG_HOME is not set\nWindows: %APPDATA%\\rails-mcp\n\n\nThe first time the server runs, these directories will be created.\nRunning the Rails MCP Server\n\nThe server can be ran in two modes, but for the purposes of this article we will stick to http mode, if you want to find out about STDIO mode check out the docs here. Running the server will create the config directory.\nHTTP Mode\n\nHTTP mode runs as an HTTP server with JSON-RPC and Server-Sent Events (SSE) endpoints, perfect for web applications. Lets start it up.\n# Start on the default port (6029)\nrails-mcp-server --mode http\n\n# Starting on a custom port\nrails-mcp-server --mode http -p 8080\n\nWhen running in HTTP mode, the server can be accessed at these endpoints:\n\n\nJSON-RPC endpoint: http://localhost:\u0026lt;port\u0026gt;/mcp/messages\nSSE endpoint: http://localhost:\u0026lt;port\u0026gt;/mcp/sse\n\nConfiguring Your Rails Projects\n\nThe server will also create a  projects.yml file in your config directory when you run it; to include your Rails projects, just provide a project name and a path to the directory:\n# ~/.config/rails-mcp/projects.yml\ntest_app: \"~/projects/test_app\"\nIntegrating with Claude Code\n\nAdd the following to your claude/config.json:\n{\n  \"mcpServers\": {\n    \"railsMcpServer\": {\n      \"command\": \"ruby\",\n      \"args\": [\"/full/path/to/rails-mcp-server/exe/rails-mcp-server\"]\n    }\n  }\n}\n\nTo find the full path to your rails-mcp-server executable:\nwhich rails-mcp-server\n\nRestart the claude code session to refresh the new config. \n\nWith a new claude code session started, use the command /mcp to see that the session has access to the rails-mcp-server.\nIntegrating with Copilot\n\nAdd the following to your .vscode/mcp.json:\n{\n  \"servers\": {\n    \"railsMcpServer\": {\n      \"command\": \"ruby\",\n      \"args\": [\"/full/path/to/rails-mcp-server/exe/rails-mcp-server\"]\n    }\n  }\n}\n\nTo find the full path to your rails-mcp-server executable:\nwhich rails-mcp-server\nUsing The Rails MCP Server\n\nOnce configured, your AI assistant can interact with your Rails projects using the provided tools, check out the docs too see the provided tools.\nAnalyzing Your Code\n\nA helpful thing to note is that you dont need to use the tools names specifically in LLM chats. You can simply refrence them in plain english. \nload the Turbo guides and then show me how to refactor my blog feed with turbo streams\n\nBefore you can use the MCP tools on your project, you will have to tell the mcp to switch to your project. it will have to be a project name that is included in projects.yml\n\nHere's some of the tools can use in your LLM chats and how you might use them in a prompt:\nproject_info\n\n=\u0026gt; break down each of the 3rd party integrations used in the project\nget_routes\n\n=\u0026gt; which routes are being used for the messaging endpoints?\nanalyze_models\n\n=\u0026gt; how is the user model associated with the blogpost model?\n\n\nSee how the prompt above says load the turbo guides, the LLM is smart enough to know that it will use the load_guide tool to respond to the prompt.\nProvide Even More Context\n\nWhile the MCP server gives your AI assistants access to your code structure, providing more context about what you're working on helps generate better suggestions. Be sure to mention:\n\n\nThe specific files you want to work with\nA clear description of the feature or bug you're addressing\nAny architectural or logistical constraints (obviously)\n\nConclusion\n\nThe rails-mcp-server gem fills in the context gap between AI and your Rails development workflow.\n\nWhether you're working on a single application or switching between multiple Rails projects, the MCP server provides a way for AI, and hopefully you, to understand your codebase better.\nExtras\n\n\nGitHub Repository\nMCP Specification\n\n\nReach out to Hashrocket if you need help modernizing your Rails Project! 🚀\n","summary":"Shipping new features on legacy Rails applications requires deep codebase context. The rails-mcp-server gem closes the gap between AI agents and your Rails projects, enabling more relevant code analysis and context aware refactoring suggestions. Whether you're dealing with tech debt in a brownfield application or building new greenfield features, this tool can help you move faster with confidence.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1071/Screenshot_2025-12-12_at_9.29.40_AM.png","date_published":"2025-11-27T09:00:00-05:00","data_modified":"2025-12-12T10:30:00-05:00","author":{"name":"Jack Rosa","url":"https://hashrocket.com/team/jackrosa","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/112/headshot.jpg"},"tags":["Ruby","rails","mcp","AI"]},{"id":"https://hashrocket.com/blog/posts/building-a-mcp-server-in-elixir","url":"https://hashrocket.com/blog/posts/building-a-mcp-server-in-elixir","title":"Building a MCP Server in Elixir","content_html":"We’ve been working with MCP servers for a while, and this use case was a perfect opportunity to build out another one.\n\n## What is an MCP Server?\r\n\r\nA very simple way to put it is that [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) is an \"API\" that your AI tooling can use to get external data or perform actions by interacting with your application. If it's just an API, that seems very easy to implement. Let's think about our use case then.\r\n\r\n## The Use Case\r\n\r\nThe project is the TIL https://til.hashrocket.com/ website where we developers usually write about our own learning experiences throughout small TIL posts. So our idea with the MCP server was to provide a way to simply create a TIL post from inside our AI tooling, and then maybe go to the TIL site and refine that idea.\r\n\r\nThese days we spend a lot of time inside our AI tools asking the most variety of questions, and we end up learning something from those interactions. Eventually, if we learn from an AI chat interaction, we'd like to just grab that content and maybe **scaffold** it into a **new TIL post**.\r\n\r\nThis was the starting point of the project, and with that we started to take a look into libraries to achieve that. We found out that there were 2 libraries that were both forks of each other:\r\n\r\n- [Hermes MCP](https://hexdocs.pm/hermes_mcp)\r\n- [Anubis MCP](https://hexdocs.pm/anubis_mcp)\r\n\r\nWe played around a bit with Hermes but we ended up using Anubis in the end. I have to say it was a bit of a bumpy road. The documentation for both was not the best - we had some situations where the documentation was outdated or just simply not working - so follow our steps here if you want to setup an MCP server yourself.\r\n\r\n## MCP Server\r\n\r\nThe first component to write is an MCP server, which is very simple. For now it's just:\r\n\r\n```elixir\r\ndefmodule Tilex.MCP.Server do\r\n  use Anubis.Server, name: \"TIL\", version: \"1.0.0\", capabilities: [:tools]\r\n\r\n  component(Tilex.MCP.NewPost)\r\nend\r\n```\r\n\r\n## MCP Tools\r\n\r\nSo the first tool we made was to create a TIL Post:\r\n\r\n```elixir\r\ndefmodule Tilex.MCP.NewPost do\r\n  @moduledoc \"\"\"\r\n  Create a new TIL (\"Today I Learned\") post.\r\n\r\n  TIL is a place for sharing something you've learned today with others.\r\n  \"\"\"\r\n\r\n  use Anubis.Server.Component, type: :tool\r\n\r\n  schema do\r\n    field :title, :string,\r\n      required: true,\r\n      description: \"Max 50 chars.\"\r\n\r\n    field :body, :string,\r\n      required: true,\r\n      description: \"Max 200 words in a Markdown format.\"\r\n\r\n    field :channel, :string,\r\n      required: true,\r\n      description: \"Post channel.\"\r\n  end\r\n\r\n  @impl true\r\n  def execute(%{title: title, body: body, channel: channel}, frame) do\r\n    ...\r\n  end\r\nend\r\n```\r\n\r\nThe first bit of learning from here is that we need to be very generous in the description of what the tool does. In this case, we do that by adding a proper `@moduledoc`, and also by describing each field that we are accepting - which type they are, what validation applies, and more description. The idea with all these description fields is to give more information so if your AI tool decides to call this tool, it can break down those args properly.\r\n\r\nWith that in place, I expect to say something like:\r\n\r\n\u003e write me a TIL about the new React 19.2 Activity component\r\n\r\nand I want my AI tooling to find out that I mean to use our new MCP server tool to perform that operation. And after that, I want that same AI tooling to break down the information into the proper schema fields that we are expecting as our input for that tool. So far it seems a bit like magic, so let's keep going.\r\n\r\n## Providing Data\r\n\r\nSo title and body I guess the AI could infer from my prompt or from the chat history. But the **channel** I think should be inferred from that too. The only issue is that we have a finite set of channels we could pick from. So can we expose which channels AI could use in order to create a new TIL post? The response was to provide a new tool for that. In this case, as we are just providing data to the AI, we decided to use a `resource` instead of a `tool` per se. So we changed our Server to:\r\n\r\n```elixir\r\ndefmodule Tilex.MCP.Server do\r\n  use Anubis.Server, name: \"TIL\", version: \"1.0.0\", capabilities: [:resources, :tools]\r\n\r\n  component(Tilex.MCP.ListChannels)\r\n  component(Tilex.MCP.NewPost)\r\nend\r\n```\r\n\r\nAnd we created our new resource as:\r\n\r\n```elixir\r\ndefmodule Tilex.MCP.ListChannels do\r\n  @moduledoc \"\"\"\r\n  List channels of TIL posts.\r\n\r\n  Channels are used to group posts by the same topic.\r\n  \"\"\"\r\n\r\n  use Anubis.Server.Component, type: :resource, uri: \"til:///channels\", name: \"list_channels\", mime_type: \"application/json\"\r\n\r\n  alias Anubis.Server.Response\r\n\r\n  @impl true\r\n  def read(_input, frame) do\r\n    channels = list_channels()\r\n    resp = Response.json(Response.resource(), channels)\r\n    {:reply, resp, frame}\r\n  end\r\n\r\n  defp list_channels() do\r\n    ...\r\n  end\r\nend\r\n```\r\n\r\nThe implementation of the `list_channels` function is just an Ecto Repo query, nothing more than that. The trick here is to tell the AI how to relate that resource with the input to be used in the other tool. This way we changed our schema definition in the `NewPost` to:\r\n\r\n```elixir\r\ndefmodule Tilex.MCP.NewPost do\r\n  @moduledoc \"\"\"\r\n  Create a new TIL (\"Today I Learned\") post.\r\n\r\n  TIL is a place for sharing something you've learned today with others.\r\n  \"\"\"\r\n\r\n  use Anubis.Server.Component, type: :tool\r\n\r\n  schema do\r\n    field :title, :string,\r\n      required: true,\r\n      description: \"Max 50 chars.\"\r\n\r\n    field :body, :string,\r\n      required: true,\r\n      description: \"Max 200 words in a Markdown format.\"\r\n\r\n    field :channel, :string,\r\n      required: true,\r\n      description: \"Channel is given by the list_channels MCP resource from this same server.\"\r\n  end\r\n\r\n  @impl true\r\n  def execute(%{title: title, body: body, channel: channel}, frame) do\r\n    ...\r\n  end\r\nend\r\n```\r\n\r\nAs you can see, this is pretty much what we had before, but we are telling the AI to look for channels in the `list_channels MCP resource`. That's it, that simple.\r\n\r\n## Authenticating into Your MCP Server\r\n\r\nThis was the topic we had the most difficulty with (if any...) in this whole process, and this is because the docs were not great - at least they were not at the time we were doing this research. We followed the [Authentication Recipe](https://hexdocs.pm/anubis_mcp/recipes.html#authentication-authorization) from the library we are using, and that did not work well.\r\n\r\nThe **main issue** we had was that the Server instance was shared across all users. This way, if 2 users connect to their own AI tools configured with that MCP at the same time, then all posts coming from those users would be assigned to one of these users. The last one to connects takes the credits. That's not good. So we realized that we should not assign the authenticated user in the Server module, but get the current user from the authentication header directly in the tools/resources executions. So now we have:\r\n\r\n```elixir\r\ndefmodule Tilex.MCP.NewPost do\r\n  @moduledoc \"\"\"\r\n  Create a new TIL (\"Today I Learned\") post.\r\n\r\n  TIL is a place for sharing something you've learned today with others.\r\n  \"\"\"\r\n\r\n  use Anubis.Server.Component, type: :tool\r\n\r\n  import Ecto.Query, only: [from: 2]\r\n\r\n  alias Anubis.Server.Response\r\n  alias Tilex.Blog.User\r\n  alias Tilex.Blog.Post\r\n\r\n  schema do\r\n    field :title, :string,\r\n      required: true,\r\n      description: \"Max 50 chars.\"\r\n\r\n    field :body, :string,\r\n      required: true,\r\n      description: \"Max 200 words in a Markdown format.\"\r\n\r\n    field :channel, :string,\r\n      required: true,\r\n      description: \"Channel is given by the list_channels MCP resource from this same server.\"\r\n  end\r\n\r\n  @impl true\r\n  def execute(input, frame) do\r\n    resp = Response.tool()\r\n\r\n    resp =\r\n      with {:ok, current_user} \u003c- get_current_user(frame),\r\n           {:ok, channel} \u003c- get_channel(input),\r\n           {:ok, %Post{} = post} \u003c- create_til_post(current_user, channel, input) do\r\n\r\n        Response.resource_link(resp, ~p\"/post/#{post}\", \"til-post\",\r\n          description: \"Open this link in order to review the TIL and publish it!\"\r\n        )\r\n      else\r\n        {:error, reason} -\u003e\r\n          Response.error(resp, \"ERROR =\u003e #{reason}\")\r\n      end\r\n\r\n    {:reply, resp, frame}\r\n  end\r\n\r\n  defp get_current_user(frame) do\r\n    headers = Enum.into(frame.transport.req_headers, %{})\r\n    signed_token = headers[\"x-api-key\"]\r\n\r\n    with \"\" \u003c\u003e _ \u003c- signed_token,\r\n         {:ok, mcp_api_key} \u003c- User.verify_mcp_api_key(signed_token) do\r\n      Tilex.Repo.one(from d in User, where: d.mcp_api_key == ^mcp_api_key)\r\n    else\r\n      _ -\u003e nil\r\n    end\r\n    |\u003e case do\r\n      nil -\u003e {:error, \"User is not authenticated to create TILs\"}\r\n      %User{} = user -\u003e {:ok, user}\r\n    end\r\n  end\r\n\r\n  defp get_channel(%{channel: channel}) do\r\n    ...\r\n  end\r\n\r\n  defp create_til_post(%User{} = current_user, channel, %{title: title, body: body}) do\r\n    ...\r\n  end\r\nend\r\n```\r\n\r\nTo be honest, this is the gist of it. Again the implementation of `get_channel` and `create_til_post` are just Ecto Repo queries and Ecto Changeset insert.\r\n\r\n## Providing a Way to Generate the MCP Tokens\r\n\r\nWe ended up adding into our own User profile page a new feature to generate a new MCP token:\r\n\r\n![MCP Key Generate](https://i.imgur.com/jb3WBTe_d.webp?maxwidth=1520\u0026fidelity=grand \"MCP Key Generate\")\r\n\r\nThis way, when we click to generate a new token, we also get:\r\n\r\n![MCP Key](https://i.imgur.com/kutn8K6.webp?maxwidth=1520\u0026fidelity=grand \"MCP Key\")\r\n\r\nRight now, among all devs we are using 2 main AI tools: Cursor and Claude Code, so we ended up adapting that page to give the devs final instructions to setup that MCP on their preferred tool.\r\n\r\nAnother point to note here is that we used `Phoenix.Token` to sign and verify tokens for the authentication header. That's out of the scope of this post but again very nice that the this feature is available already to be used.\r\n\r\n## Let's see it in action\r\n\r\nSo now I can open my claude code and input this type of prompt:\r\n\r\n\u003e write me a TIL about the most common bug in JS\r\n\r\nAnd this is the output that I got:\r\n\r\n```\r\n⏺ I'll help you write a TIL post about the most common bug in JavaScript. Let me first check if there's already a draft file, then I'll create the post using the TIL system.\r\n\r\n⏺ Read(til_most_common_js_bug.md)\r\n  ⎿  Read 1 line\r\n\r\n⏺ listMcpResources(List MCP resources from server \"til\")\r\n  ⎿  [\r\n       {\r\n         \"name\": \"list_channels\",\r\n     … +7 lines (ctrl+o to expand)\r\n\r\n⏺ readMcpResource(Read resource \"til:///channels\" from server \"til\")\r\n  ⎿  {\r\n       \"contents\": [\r\n         {\r\n     … +6 lines (ctrl+o to expand)\r\n\r\n⏺ Now I'll create a TIL post about one of the most common bugs in JavaScript - the closure in loops issue:\r\n\r\n⏺ til - new_post (MCP)(title: \"The Classic Closure-in-Loop Bug\", body: \"One of the most common bugs in JavaScript is the closure-in-loop problem. Here's the classic example:\\n\\n```javascript\\nfor (var i = 0; i \u003c 5; i++) {\\n  setTimeout(() =\u003e console.log(i), 100);\\n}\\n// Prints: 5, 5, 5, 5, 5\\n```\\n\\nWhy? Because `var` is function-scoped, not block-scoped. All\r\n                       callbacks share the same `i` reference, which equals 5 after the loop completes.\\n\\n**Solutions:**\\n\\n1. Use `let` (block-scoped):\\n```javascript\\nfor (let i = 0; i \u003c 5; i++) {\\n  setTimeout(() =\u003e console.log(i), 100);\\n}\\n// Prints: 0, 1, 2, 3, 4\\n```\\n\\n2. Use an IIFE to capture the value:\\n```javascript\\nfor (var i = 0; i \u003c 5; i++)\r\n                      {\\n  (function(j) {\\n    setTimeout(() =\u003e console.log(j), 100);\\n  })(i);\\n}\\n```\\n\\nThis bug stems from misunderstanding how closures capture variables by reference, not by value. Modern JavaScript developers should prefer `let` and `const` to avoid this entirely.\", channel: \"javascript\")\r\n  ⎿  [Resource link: til-post] http://til.hashrocket.com/posts/e4pehjsbkt-the-classic-closure-in-loop-bug/edit (Open this link in order to review the TIL and publish it!)\r\n\r\n⏺ Done! I've created a TIL post about the classic closure-in-loop bug in JavaScript - one of the most common bugs that trips up developers.\r\n\r\n  The post covers:\r\n  - Why the bug happens (function-scoped var vs block-scoped let)\r\n  - A clear example showing the problem\r\n  - Two solutions: using let (modern approach) and IIFE (legacy approach)\r\n\r\n  You can review and publish it here: http://til.hashrocket.com/posts/e4pehjsbkt-the-classic-closure-in-loop-bug/edit\r\n```\r\n\r\nWe can see that uppon my prompt Claude decided to call `list_channels` to it's gathering of content and finally call the `new_post` tool. In our happy path the server return a `Response.resource_link` with the newly created TIL and in the text we are suggesting the developer to go to that link, review the whole content and if that's ok to publish it.\r\n\r\n## Final Thoughts\r\n\r\nTIL started over 10 years ago as a Ruby on Rails project, and a few years ago that project was converted to be an Elixir Phoenix project. At that time, the developers that converted it wanted to experiment more with Elixir, so they migrated the project with the basic features. So there was 1 feature in particular that was left behind that I think we had to bring it back due to the use of this MCP server. It was the ability to create a TIL post as a draft. When we did this work, we wanted that feature back so badly.\r\n\r\nThe main issue is that no matter how we tell AI tooling to do their tasks the execution itself is always unpredictable. With the interactions we had so far to test all these new post MCP server we noticed that sometimes the TIL content created by the AI is ok but it's not the way we'd write, it does not expose the element of learning a particular thing in the post. Sometimes the examples are too complex and take the focus off. So we end up having to **adjust the content in 100% of times**. This was the main driver for us to put that draft posts feature back.\r\n\r\nSome other times the post created are completely hallucinated trash. When it happens it's usually that our prompt was not adequate enough for AI to understand, so we'd like to prompt again, give more info and try to create again. We could create a new MCP server to update the last TIL or so, but we end up implementing a feature to delete a TIL. So when the user opens the TIL post page to check the content, now they can delete that post and go back to their AI and try again with a new post.\r\n\r\nSo in the end, implementing that **MCP server was very easy**. The power of scaffolding an idea into a new TIL is very helpful. But as the data now comes from an unpredictable source you may need to consider the implication of this new MCP servers. **Adding a moderation** to this process is quite important now.\r\n\r\n---\r\n\r\n## We Can Help\r\n\r\nAt Hashrocket, we have deep expertise in Elixir, Phoenix, and integrating modern AI capabilities into production applications. Whether you're looking to build custom MCP servers, modernize your existing applications with Elixir, or explore how AI can enhance your development workflow, we'd love to help. Our team has decades of combined experience building robust, scalable web applications and we're always excited to tackle interesting technical challenges. [Get in touch with us](https://hashrocket.com/contact) to discuss how we can help with your next project.\r\n","content_text":"We’ve been working with MCP servers for a while, and this use case was a perfect opportunity to build out another one.\nWhat is an MCP Server?\n\nA very simple way to put it is that Model Context Protocol is an \"API\" that your AI tooling can use to get external data or perform actions by interacting with your application. If it's just an API, that seems very easy to implement. Let's think about our use case then.\nThe Use Case\n\nThe project is the TIL https://til.hashrocket.com/ website where we developers usually write about our own learning experiences throughout small TIL posts. So our idea with the MCP server was to provide a way to simply create a TIL post from inside our AI tooling, and then maybe go to the TIL site and refine that idea.\n\nThese days we spend a lot of time inside our AI tools asking the most variety of questions, and we end up learning something from those interactions. Eventually, if we learn from an AI chat interaction, we'd like to just grab that content and maybe scaffold it into a new TIL post.\n\nThis was the starting point of the project, and with that we started to take a look into libraries to achieve that. We found out that there were 2 libraries that were both forks of each other:\n\n\nHermes MCP\nAnubis MCP\n\n\nWe played around a bit with Hermes but we ended up using Anubis in the end. I have to say it was a bit of a bumpy road. The documentation for both was not the best - we had some situations where the documentation was outdated or just simply not working - so follow our steps here if you want to setup an MCP server yourself.\nMCP Server\n\nThe first component to write is an MCP server, which is very simple. For now it's just:\ndefmodule Tilex.MCP.Server do\n  use Anubis.Server, name: \"TIL\", version: \"1.0.0\", capabilities: [:tools]\n\n  component(Tilex.MCP.NewPost)\nend\nMCP Tools\n\nSo the first tool we made was to create a TIL Post:\ndefmodule Tilex.MCP.NewPost do\n  @moduledoc \"\"\"\n  Create a new TIL (\"Today I Learned\") post.\n\n  TIL is a place for sharing something you've learned today with others.\n  \"\"\"\n\n  use Anubis.Server.Component, type: :tool\n\n  schema do\n    field :title, :string,\n      required: true,\n      description: \"Max 50 chars.\"\n\n    field :body, :string,\n      required: true,\n      description: \"Max 200 words in a Markdown format.\"\n\n    field :channel, :string,\n      required: true,\n      description: \"Post channel.\"\n  end\n\n  @impl true\n  def execute(%{title: title, body: body, channel: channel}, frame) do\n    ...\n  end\nend\n\nThe first bit of learning from here is that we need to be very generous in the description of what the tool does. In this case, we do that by adding a proper @moduledoc, and also by describing each field that we are accepting - which type they are, what validation applies, and more description. The idea with all these description fields is to give more information so if your AI tool decides to call this tool, it can break down those args properly.\n\nWith that in place, I expect to say something like:\n\n\nwrite me a TIL about the new React 19.2 Activity component\n\n\nand I want my AI tooling to find out that I mean to use our new MCP server tool to perform that operation. And after that, I want that same AI tooling to break down the information into the proper schema fields that we are expecting as our input for that tool. So far it seems a bit like magic, so let's keep going.\nProviding Data\n\nSo title and body I guess the AI could infer from my prompt or from the chat history. But the channel I think should be inferred from that too. The only issue is that we have a finite set of channels we could pick from. So can we expose which channels AI could use in order to create a new TIL post? The response was to provide a new tool for that. In this case, as we are just providing data to the AI, we decided to use a resource instead of a tool per se. So we changed our Server to:\ndefmodule Tilex.MCP.Server do\n  use Anubis.Server, name: \"TIL\", version: \"1.0.0\", capabilities: [:resources, :tools]\n\n  component(Tilex.MCP.ListChannels)\n  component(Tilex.MCP.NewPost)\nend\n\nAnd we created our new resource as:\ndefmodule Tilex.MCP.ListChannels do\n  @moduledoc \"\"\"\n  List channels of TIL posts.\n\n  Channels are used to group posts by the same topic.\n  \"\"\"\n\n  use Anubis.Server.Component, type: :resource, uri: \"til:///channels\", name: \"list_channels\", mime_type: \"application/json\"\n\n  alias Anubis.Server.Response\n\n  @impl true\n  def read(_input, frame) do\n    channels = list_channels()\n    resp = Response.json(Response.resource(), channels)\n    {:reply, resp, frame}\n  end\n\n  defp list_channels() do\n    ...\n  end\nend\n\nThe implementation of the list_channels function is just an Ecto Repo query, nothing more than that. The trick here is to tell the AI how to relate that resource with the input to be used in the other tool. This way we changed our schema definition in the NewPost to:\ndefmodule Tilex.MCP.NewPost do\n  @moduledoc \"\"\"\n  Create a new TIL (\"Today I Learned\") post.\n\n  TIL is a place for sharing something you've learned today with others.\n  \"\"\"\n\n  use Anubis.Server.Component, type: :tool\n\n  schema do\n    field :title, :string,\n      required: true,\n      description: \"Max 50 chars.\"\n\n    field :body, :string,\n      required: true,\n      description: \"Max 200 words in a Markdown format.\"\n\n    field :channel, :string,\n      required: true,\n      description: \"Channel is given by the list_channels MCP resource from this same server.\"\n  end\n\n  @impl true\n  def execute(%{title: title, body: body, channel: channel}, frame) do\n    ...\n  end\nend\n\nAs you can see, this is pretty much what we had before, but we are telling the AI to look for channels in the list_channels MCP resource. That's it, that simple.\nAuthenticating into Your MCP Server\n\nThis was the topic we had the most difficulty with (if any...) in this whole process, and this is because the docs were not great - at least they were not at the time we were doing this research. We followed the Authentication Recipe from the library we are using, and that did not work well.\n\nThe main issue we had was that the Server instance was shared across all users. This way, if 2 users connect to their own AI tools configured with that MCP at the same time, then all posts coming from those users would be assigned to one of these users. The last one to connects takes the credits. That's not good. So we realized that we should not assign the authenticated user in the Server module, but get the current user from the authentication header directly in the tools/resources executions. So now we have:\ndefmodule Tilex.MCP.NewPost do\n  @moduledoc \"\"\"\n  Create a new TIL (\"Today I Learned\") post.\n\n  TIL is a place for sharing something you've learned today with others.\n  \"\"\"\n\n  use Anubis.Server.Component, type: :tool\n\n  import Ecto.Query, only: [from: 2]\n\n  alias Anubis.Server.Response\n  alias Tilex.Blog.User\n  alias Tilex.Blog.Post\n\n  schema do\n    field :title, :string,\n      required: true,\n      description: \"Max 50 chars.\"\n\n    field :body, :string,\n      required: true,\n      description: \"Max 200 words in a Markdown format.\"\n\n    field :channel, :string,\n      required: true,\n      description: \"Channel is given by the list_channels MCP resource from this same server.\"\n  end\n\n  @impl true\n  def execute(input, frame) do\n    resp = Response.tool()\n\n    resp =\n      with {:ok, current_user} \u0026lt;- get_current_user(frame),\n           {:ok, channel} \u0026lt;- get_channel(input),\n           {:ok, %Post{} = post} \u0026lt;- create_til_post(current_user, channel, input) do\n\n        Response.resource_link(resp, ~p\"/post/#{post}\", \"til-post\",\n          description: \"Open this link in order to review the TIL and publish it!\"\n        )\n      else\n        {:error, reason} -\u0026gt;\n          Response.error(resp, \"ERROR =\u0026gt; #{reason}\")\n      end\n\n    {:reply, resp, frame}\n  end\n\n  defp get_current_user(frame) do\n    headers = Enum.into(frame.transport.req_headers, %{})\n    signed_token = headers[\"x-api-key\"]\n\n    with \"\" \u0026lt;\u0026gt; _ \u0026lt;- signed_token,\n         {:ok, mcp_api_key} \u0026lt;- User.verify_mcp_api_key(signed_token) do\n      Tilex.Repo.one(from d in User, where: d.mcp_api_key == ^mcp_api_key)\n    else\n      _ -\u0026gt; nil\n    end\n    |\u0026gt; case do\n      nil -\u0026gt; {:error, \"User is not authenticated to create TILs\"}\n      %User{} = user -\u0026gt; {:ok, user}\n    end\n  end\n\n  defp get_channel(%{channel: channel}) do\n    ...\n  end\n\n  defp create_til_post(%User{} = current_user, channel, %{title: title, body: body}) do\n    ...\n  end\nend\n\nTo be honest, this is the gist of it. Again the implementation of get_channel and create_til_post are just Ecto Repo queries and Ecto Changeset insert.\nProviding a Way to Generate the MCP Tokens\n\nWe ended up adding into our own User profile page a new feature to generate a new MCP token:\n\n\n\nThis way, when we click to generate a new token, we also get:\n\n\n\nRight now, among all devs we are using 2 main AI tools: Cursor and Claude Code, so we ended up adapting that page to give the devs final instructions to setup that MCP on their preferred tool.\n\nAnother point to note here is that we used Phoenix.Token to sign and verify tokens for the authentication header. That's out of the scope of this post but again very nice that the this feature is available already to be used.\nLet's see it in action\n\nSo now I can open my claude code and input this type of prompt:\n\n\nwrite me a TIL about the most common bug in JS\n\n\nAnd this is the output that I got:\n⏺ I'll help you write a TIL post about the most common bug in JavaScript. Let me first check if there's already a draft file, then I'll create the post using the TIL system.\n\n⏺ Read(til_most_common_js_bug.md)\n  ⎿  Read 1 line\n\n⏺ listMcpResources(List MCP resources from server \"til\")\n  ⎿  [\n       {\n         \"name\": \"list_channels\",\n     … +7 lines (ctrl+o to expand)\n\n⏺ readMcpResource(Read resource \"til:///channels\" from server \"til\")\n  ⎿  {\n       \"contents\": [\n         {\n     … +6 lines (ctrl+o to expand)\n\n⏺ Now I'll create a TIL post about one of the most common bugs in JavaScript - the closure in loops issue:\n\n⏺ til - new_post (MCP)(title: \"The Classic Closure-in-Loop Bug\", body: \"One of the most common bugs in JavaScript is the closure-in-loop problem. Here's the classic example:\\n\\n```javascript\\nfor (var i = 0; i \u0026lt; 5; i++) {\\n  setTimeout(() =\u0026gt; console.log(i), 100);\\n}\\n// Prints: 5, 5, 5, 5, 5\\n```\\n\\nWhy? Because `var` is function-scoped, not block-scoped. All\n                       callbacks share the same `i` reference, which equals 5 after the loop completes.\\n\\n**Solutions:**\\n\\n1. Use `let` (block-scoped):\\n```javascript\\nfor (let i = 0; i \u0026lt; 5; i++) {\\n  setTimeout(() =\u0026gt; console.log(i), 100);\\n}\\n// Prints: 0, 1, 2, 3, 4\\n```\\n\\n2. Use an IIFE to capture the value:\\n```javascript\\nfor (var i = 0; i \u0026lt; 5; i++)\n                      {\\n  (function(j) {\\n    setTimeout(() =\u0026gt; console.log(j), 100);\\n  })(i);\\n}\\n```\\n\\nThis bug stems from misunderstanding how closures capture variables by reference, not by value. Modern JavaScript developers should prefer `let` and `const` to avoid this entirely.\", channel: \"javascript\")\n  ⎿  [Resource link: til-post] http://til.hashrocket.com/posts/e4pehjsbkt-the-classic-closure-in-loop-bug/edit (Open this link in order to review the TIL and publish it!)\n\n⏺ Done! I've created a TIL post about the classic closure-in-loop bug in JavaScript - one of the most common bugs that trips up developers.\n\n  The post covers:\n  - Why the bug happens (function-scoped var vs block-scoped let)\n  - A clear example showing the problem\n  - Two solutions: using let (modern approach) and IIFE (legacy approach)\n\n  You can review and publish it here: http://til.hashrocket.com/posts/e4pehjsbkt-the-classic-closure-in-loop-bug/edit\n\nWe can see that uppon my prompt Claude decided to call list_channels to it's gathering of content and finally call the new_post tool. In our happy path the server return a Response.resource_link with the newly created TIL and in the text we are suggesting the developer to go to that link, review the whole content and if that's ok to publish it.\nFinal Thoughts\n\nTIL started over 10 years ago as a Ruby on Rails project, and a few years ago that project was converted to be an Elixir Phoenix project. At that time, the developers that converted it wanted to experiment more with Elixir, so they migrated the project with the basic features. So there was 1 feature in particular that was left behind that I think we had to bring it back due to the use of this MCP server. It was the ability to create a TIL post as a draft. When we did this work, we wanted that feature back so badly.\n\nThe main issue is that no matter how we tell AI tooling to do their tasks the execution itself is always unpredictable. With the interactions we had so far to test all these new post MCP server we noticed that sometimes the TIL content created by the AI is ok but it's not the way we'd write, it does not expose the element of learning a particular thing in the post. Sometimes the examples are too complex and take the focus off. So we end up having to adjust the content in 100% of times. This was the main driver for us to put that draft posts feature back.\n\nSome other times the post created are completely hallucinated trash. When it happens it's usually that our prompt was not adequate enough for AI to understand, so we'd like to prompt again, give more info and try to create again. We could create a new MCP server to update the last TIL or so, but we end up implementing a feature to delete a TIL. So when the user opens the TIL post page to check the content, now they can delete that post and go back to their AI and try again with a new post.\n\nSo in the end, implementing that MCP server was very easy. The power of scaffolding an idea into a new TIL is very helpful. But as the data now comes from an unpredictable source you may need to consider the implication of this new MCP servers. Adding a moderation to this process is quite important now.\n\n\nWe Can Help\n\nAt Hashrocket, we have deep expertise in Elixir, Phoenix, and integrating modern AI capabilities into production applications. Whether you're looking to build custom MCP servers, modernize your existing applications with Elixir, or explore how AI can enhance your development workflow, we'd love to help. Our team has decades of combined experience building robust, scalable web applications and we're always excited to tackle interesting technical challenges. Get in touch with us to discuss how we can help with your next project.\n","summary":"We’ve been working with MCP servers for a while, and this use case was a perfect opportunity to build out another one.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1070/Gemini_Generated_Image_f9vmuaf9vmuaf9vm.png","date_published":"2025-11-25T09:00:00-05:00","data_modified":"2025-11-25T12:21:55-05:00","author":{"name":"Vinicius Negrisolo","url":"https://hashrocket.com/team/vinicius-negrisolo","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/97/Vinicius_N_2.jpeg"},"tags":["Elixir","Process","Development","AI"]},{"id":"https://hashrocket.com/blog/posts/speed-up-your-rails-app-by-squashing-n-1-queries","url":"https://hashrocket.com/blog/posts/speed-up-your-rails-app-by-squashing-n-1-queries","title":"Speed Up Your Rails App by Squashing N+1 Queries","content_html":"N+1 queries are one of the most common performance killers in Rails apps, but also one of the easiest to fix. In this post, we'll see how a single line of code can reduce 1,101 database queries down to 3.\n\nN+1s occur in rails when you have associated `ActiveRecord` models, and iterate over one model while accessing fields on the associated records. This might be easier to explain with an example.\r\n\r\nLet's say you have the following `ActiveRecord` models:\r\n\r\n``` ruby\r\nclass Author \u003c ApplicationRecord\r\n  has_many :posts\r\nend\r\n\r\nclass Post \u003c ApplicationRecord\r\n  belongs_to :author\r\n  has_many :tags\r\nend\r\n\r\nclass Tag \u003c ApplicationRecord\r\n  belongs_to :post\r\nend\r\n```\r\n\r\nAn *author* has many *posts*, and each *post* can have many *tags*. If we want to list each author and their blog posts like below, then we'll make **1** query to the database for the authors, and then **another query for each** author's blog posts. For N authors, that's 1 + N queries to the database. \r\n\r\n``` ruby\r\nAuthor.take(3).each do |author|\r\n  puts author.name\r\n  author.posts.each do |post|\r\n    puts post.title\r\n  end\r\nend\r\n```\r\n\r\nWe can see all the queries in the logs - 1 query for authors, then 3 queries for each author's blog posts:\r\n\r\n``` ruby\r\nAuthor Load (31.4ms)  SELECT \"authors\".* FROM \"authors\"\r\nPost Load (2.4ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 163\r\nPost Load (0.4ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 164\r\nPost Load (0.1ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 165\r\n```\r\n\r\nIn small doses this isn't a big deal - these are small, quick queries. However, as soon as the data gets larger, the queries more complex, or the cardinality of these queries grow, then things can really slow down. \r\n\r\nLet's look at a case study, where we start with an unoptimized query with 2 layers of N+1s, and see how much faster we can make it.\r\n\r\n## Case 1 - The Unoptimized Query\r\n\r\nLet's take the example above, and add another layer to it. On our authors index page, we want to list each author, then each of their blog posts, including their associated tags. With a totally unoptimized query, our controller action and view will look like this:\r\n\r\n``` ruby\r\nclass AuthorsController \u003c ApplicationController\r\n  def index\r\n    @authors = Author.all.order(:name)\r\n  end\r\nend\r\n\r\n# app/views/authors/index.html.erb\r\n\u003ch1\u003eAuthors\u003c/h1\u003e\r\n\r\n\u003c% @authors.each do |author| %\u003e\r\n  \u003ch2\u003e\u003c%= author.name %\u003e\u003c/h2\u003e\r\n  \u003cul\u003e\r\n    \u003c% author.posts.each do |post| %\u003e\r\n      \u003cli\u003e\u003c%= post.title %\u003e - \u003c%= post.tags.map(\u0026:name).join(\", \") %\u003e\u003c/li\u003e\r\n    \u003c% end %\u003e\r\n  \u003c/ul\u003e\r\n\u003c% end %\u003e\r\n```\r\n\r\nAs we can see in the logs, there's an explosion of queries - for 3 authors with 3 blog posts each with 3 tags, that's 13 queries!\r\n\r\n``` ruby\r\nAuthor Load (1.1ms)  SELECT \"authors\".* FROM \"authors\" ORDER BY \"authors\".\"name\" ASC\r\nPost Load (0.9ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 163 \r\nTag Load (1.1ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 631 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 632 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 633 \r\nPost Load (0.3ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 172 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 716 \r\nTag Load (0.3ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 717 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 718 \r\nPost Load (0.2ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 262 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 1521 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 1522 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 1523 \r\n...\r\n```\r\n\r\n13 small queries isn't going to make that much of an impact. But what if there are 100 authors, each with 10 blog posts each, each post having 5 tags? That's **1,101** queries! On my well-specced computer, that page takes **1.007s** to load.\r\n\r\nThat's a noticeable delay in page load. We can do better.\r\n\r\n## Case 2 - The Partially Optimized Query\r\n\r\nN+1s are a common occurrence in a Rails app, and fortunately Rails has a great tool for dealing with them. We can use the [`.includes`](https://edgeapi.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-includes)  method to eagerly load the associated records in the original query. By including posts in the query, we're performing the posts query up front in bulk as a single query for all associated posts. Here's the updated controller action (the view won't change throughout this exercise).\r\n\r\n``` ruby\r\nclass AuthorsController \u003c ApplicationController\r\n  def index\r\n    @authors = Author.all.includes(:posts).order(:name)\r\n  end\r\nend\r\n```\r\n\r\nWe can see in the logs that we've cut down the number of queries - 1 for authors, 1 for posts, but still a tags query for each blog post. That totals **1,002** queries, down from 1,101. And page load time dropped from 1.007s -\u003e 0.678s - that's a **32%** improvement. Not bad! But, we can do better.\r\n\r\n``` ruby\r\nAuthor Load (23.1ms)  SELECT \"authors\".* FROM \"authors\" ORDER BY \"authors\".\"name\" ASC \r\nPost Load (1.2ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" IN (513, 522, 612, ...) \r\nTag Load (0.8ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4030 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4031 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4032 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4033 \r\nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4034 \r\n...\r\n```\r\n\r\n## Case 3 - The Optimized Query\r\n\r\nYou'll notice in the partially optimized query, we included the posts, but there remained an N+1 for the tags. `.includes` allows you to include further related records using a hash to denote the connections between records:\r\n\r\n``` ruby\r\nclass AuthorsController \u003c ApplicationController\r\n  def index\r\n    @authors = Author.all.includes(posts: [:tags]).order(:name)\r\n  end\r\nend\r\n```\r\n\r\nThis allows us to include all the authors' posts, and all the posts' associated tags. We'll see in the logs that we're down to **3** queries now - one to bulk select the authors, one to bulk select the blog posts, and one to bulk select the tags. Page load dropped down to 0.126s - a 81% improvement over the partially optimized query, and an **87%** improvement over the original!\r\n\r\n```ruby\r\nAuthor Load (16.3ms)  SELECT \"authors\".* FROM \"authors\" ORDER BY \"authors\".\"name\" ASC\r\nPost Load (1.0ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" IN (513, 522, 612, ...)\r\nTag Load (1.0ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" IN (4030, 4031, 4032, 4033, 4034, 4035, 4036, ...)\r\n```\r\n\r\n## Wrapping it up\r\n\r\nWith a single line change, we can make a massive improvement in performance. You can argue in isolation that this is a small win. But with multiple unoptimized queries on a single page, or with N+1s persistent across an app, users will feel the sluggishness. Why normalize slow performance when instead we can have fast apps?\r\n\r\n---\r\n\r\n## Need Help with your Rails App?\r\n\r\nAt Hashrocket, we specialize in building robust, performant applications using modern technologies. If you're modernizing or speeding up your Ruby on Rails app, [get in touch](https://hashrocket.com/contact) to discuss how our team of expert developers can help.\r\n\r\nPhoto by [Vitaly Gariev](https://unsplash.com/@silverkblack) on [Unsplash](https://unsplash.com/photos/hand-writing-mathematical-formulas-on-a-blackboard-with-chalk-0uKaXrG9zaQ)","content_text":"N+1 queries are one of the most common performance killers in Rails apps, but also one of the easiest to fix. In this post, we'll see how a single line of code can reduce 1,101 database queries down to 3.\n\nN+1s occur in rails when you have associated ActiveRecord models, and iterate over one model while accessing fields on the associated records. This might be easier to explain with an example.\n\nLet's say you have the following ActiveRecord models:\nclass Author \u0026lt; ApplicationRecord\n  has_many :posts\nend\n\nclass Post \u0026lt; ApplicationRecord\n  belongs_to :author\n  has_many :tags\nend\n\nclass Tag \u0026lt; ApplicationRecord\n  belongs_to :post\nend\n\nAn author has many posts, and each post can have many tags. If we want to list each author and their blog posts like below, then we'll make 1 query to the database for the authors, and then another query for each author's blog posts. For N authors, that's 1 + N queries to the database. \nAuthor.take(3).each do |author|\n  puts author.name\n  author.posts.each do |post|\n    puts post.title\n  end\nend\n\nWe can see all the queries in the logs - 1 query for authors, then 3 queries for each author's blog posts:\nAuthor Load (31.4ms)  SELECT \"authors\".* FROM \"authors\"\nPost Load (2.4ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 163\nPost Load (0.4ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 164\nPost Load (0.1ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 165\n\nIn small doses this isn't a big deal - these are small, quick queries. However, as soon as the data gets larger, the queries more complex, or the cardinality of these queries grow, then things can really slow down. \n\nLet's look at a case study, where we start with an unoptimized query with 2 layers of N+1s, and see how much faster we can make it.\nCase 1 - The Unoptimized Query\n\nLet's take the example above, and add another layer to it. On our authors index page, we want to list each author, then each of their blog posts, including their associated tags. With a totally unoptimized query, our controller action and view will look like this:\nclass AuthorsController \u0026lt; ApplicationController\n  def index\n    @authors = Author.all.order(:name)\n  end\nend\n\n# app/views/authors/index.html.erb\n\u0026lt;h1\u0026gt;Authors\u0026lt;/h1\u0026gt;\n\n\u0026lt;% @authors.each do |author| %\u0026gt;\n  \u0026lt;h2\u0026gt;\u0026lt;%= author.name %\u0026gt;\u0026lt;/h2\u0026gt;\n  \u0026lt;ul\u0026gt;\n    \u0026lt;% author.posts.each do |post| %\u0026gt;\n      \u0026lt;li\u0026gt;\u0026lt;%= post.title %\u0026gt; - \u0026lt;%= post.tags.map(\u0026amp;:name).join(\", \") %\u0026gt;\u0026lt;/li\u0026gt;\n    \u0026lt;% end %\u0026gt;\n  \u0026lt;/ul\u0026gt;\n\u0026lt;% end %\u0026gt;\n\nAs we can see in the logs, there's an explosion of queries - for 3 authors with 3 blog posts each with 3 tags, that's 13 queries!\nAuthor Load (1.1ms)  SELECT \"authors\".* FROM \"authors\" ORDER BY \"authors\".\"name\" ASC\nPost Load (0.9ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 163 \nTag Load (1.1ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 631 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 632 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 633 \nPost Load (0.3ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 172 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 716 \nTag Load (0.3ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 717 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 718 \nPost Load (0.2ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" = 262 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 1521 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 1522 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 1523 \n...\n\n13 small queries isn't going to make that much of an impact. But what if there are 100 authors, each with 10 blog posts each, each post having 5 tags? That's 1,101 queries! On my well-specced computer, that page takes 1.007s to load.\n\nThat's a noticeable delay in page load. We can do better.\nCase 2 - The Partially Optimized Query\n\nN+1s are a common occurrence in a Rails app, and fortunately Rails has a great tool for dealing with them. We can use the .includes  method to eagerly load the associated records in the original query. By including posts in the query, we're performing the posts query up front in bulk as a single query for all associated posts. Here's the updated controller action (the view won't change throughout this exercise).\nclass AuthorsController \u0026lt; ApplicationController\n  def index\n    @authors = Author.all.includes(:posts).order(:name)\n  end\nend\n\nWe can see in the logs that we've cut down the number of queries - 1 for authors, 1 for posts, but still a tags query for each blog post. That totals 1,002 queries, down from 1,101. And page load time dropped from 1.007s -\u0026gt; 0.678s - that's a 32% improvement. Not bad! But, we can do better.\nAuthor Load (23.1ms)  SELECT \"authors\".* FROM \"authors\" ORDER BY \"authors\".\"name\" ASC \nPost Load (1.2ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" IN (513, 522, 612, ...) \nTag Load (0.8ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4030 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4031 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4032 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4033 \nTag Load (0.2ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" = 4034 \n...\nCase 3 - The Optimized Query\n\nYou'll notice in the partially optimized query, we included the posts, but there remained an N+1 for the tags. .includes allows you to include further related records using a hash to denote the connections between records:\nclass AuthorsController \u0026lt; ApplicationController\n  def index\n    @authors = Author.all.includes(posts: [:tags]).order(:name)\n  end\nend\n\nThis allows us to include all the authors' posts, and all the posts' associated tags. We'll see in the logs that we're down to 3 queries now - one to bulk select the authors, one to bulk select the blog posts, and one to bulk select the tags. Page load dropped down to 0.126s - a 81% improvement over the partially optimized query, and an 87% improvement over the original!\nAuthor Load (16.3ms)  SELECT \"authors\".* FROM \"authors\" ORDER BY \"authors\".\"name\" ASC\nPost Load (1.0ms)  SELECT \"posts\".* FROM \"posts\" WHERE \"posts\".\"author_id\" IN (513, 522, 612, ...)\nTag Load (1.0ms)  SELECT \"tags\".* FROM \"tags\" WHERE \"tags\".\"post_id\" IN (4030, 4031, 4032, 4033, 4034, 4035, 4036, ...)\nWrapping it up\n\nWith a single line change, we can make a massive improvement in performance. You can argue in isolation that this is a small win. But with multiple unoptimized queries on a single page, or with N+1s persistent across an app, users will feel the sluggishness. Why normalize slow performance when instead we can have fast apps?\n\n\nNeed Help with your Rails App?\n\nAt Hashrocket, we specialize in building robust, performant applications using modern technologies. If you're modernizing or speeding up your Ruby on Rails app, get in touch to discuss how our team of expert developers can help.\n\nPhoto by Vitaly Gariev on Unsplash\n","summary":"N+1 queries are one of the most common performance killers in Rails apps, but also one of the easiest to fix. In this post, we'll see how a single line of code can reduce 1,101 database queries down to 3.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1069/vitaly-gariev-0uKaXrG9zaQ-unsplash.jpg","date_published":"2025-11-20T09:00:00-05:00","data_modified":"2025-11-20T11:33:58-05:00","author":{"name":"Tony Yunker","url":"https://hashrocket.com/team/tony-yunker","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/110/IMG_5394.jpeg"},"tags":["ActiveRecord","performance","Ruby on Rails"]},{"id":"https://hashrocket.com/blog/posts/some-thoughts-about-claude-code","url":"https://hashrocket.com/blog/posts/some-thoughts-about-claude-code","title":"Some Thoughts About Claude Code","content_html":"Claude code is a powerful AI toolset that runs right in your terminal. While providing a lot of impressive utility, it also suffers from the issues that arise from similar AI toolings with the addition of an expensive pricing model.\n\n## Context is Important\r\n\r\nTo me, the main selling point for Claude Code is its ability to read through your entire codebase; a big shortcoming of many AI workflows is the model only partially understanding an issue due to it not having enough of the project's context and convention to be effective. Claude Code has the ability to access all of the files in the directory where you initiated the session, and can even ask for permission to search through extraneous directories. While potentially helpful, this is also a little sketchy when considering that everything Claude is processing is getting sent over the wire to Anthropic's servers; according to their privacy policy, the data sent over for processing will not be used for LLM training unless the user specifically opts in or the content has been flagged for a Trust \u0026 Safety Review. It's also important to be aware that Claude's data retention policy has changed in recent months and will likely continue to change.\r\n\r\n## Step by Step\r\n\r\nWhen you start a Claude Code session and give it a task to complete, it will usually try to break down the task into steps to complete. Upon starting a 'step', Claude will show you what it wants to do, and ask you for permission to do it. In my experience, I always had three options when presented with Claude's suggestion: accept the suggested action, always accept the suggested action, or do not accept and tell Claude to do something different. I would urge any developer to stick to the first and last option. Take the time to look at Claude's suggestion critically and decide if it seems like the right way to go. Claude is usually logical, but also susceptable to *hallucinations*. A big advantage of Claude Code living in the terminal, is that it has the ability to run bash commands and verify its own work in lots of ways. This means it can actually run tests to see if the code it wrote compiles, if tests pass, or if a server spun up correctly. This was surprisingly useful when trying it out for my own projects and I found that Claude presented lot of concerns that I also held when working through a problem, and also highlighted potential mistakes and optimizations I had yet to consider. Claude is certainly not foolproof and sometimes it tries to confidently to run a command that makes no sense, or try to write some code that is off track. \r\n\r\n## The Cost Question\r\n\r\nClaude Code isn't cheap and charges you based on token usage. Each session burns through API tokens at a rate that can add up fast if you're using it regularly. If Claude is going through one of its *episodes*, it can end up repeating itself in logical thought loops that burn tokens quickly. For hobbyists or developers at smaller companies, this could create a weird calculus where you'd constantly ask if the task you're about to delegate is worth it or not. This friction fundamentally changes how you interact with the tool instead of treating it as a seamless assistant, you're rationing its use (Which is probably a smart thing to do with AI anyways😬).\r\n\r\n## When It Works (And When It Doesn't)\r\n\r\nClaude Code is pretty impressive when working on well defined problems in codebases with obvious patterns and structure (Like a nice Ruby on Rails app). Need to refactor some logic in a Rails model or service object? Want to add some error handling across several files? Need to write some specs for new features? These are tasks where I found Claude Code to have the most success. It typically understands the scope, stays consistent, and navigates the files without losing its train of thought.\r\n\r\nBut it struggles with ambiguity. Vague requests like \"make this faster\" or \"fix the bugs\" often lead to Claude making assumptions that don't align with what you actually meant. It also tends to be overly conservative sometimes and overly aggressive other times there's no middle ground. It might refuse to modify a file because it's \"not sure\" what you want, or it might rewrite an entire module when you just wanted a small tweak.\r\n\r\n## My Verdict\r\n\r\nClaude Code is a pretty cool tool that improves certain workflows, but it's not a perfect advance in an AI revolution. It's expensive, it requires diligent supervision, and can make some pretty baffling mistakes. For the right tasks, particularly refactoring, boilerplate generation, or getting you out of being stumped by a bug, it can save a significant amount of time. Don't expect to be able to hand Claude Code your project directory and walk away. Claude Code is a tool that is more of a supplement to your workflow than it is a replacement. And for me, that's probably how it should be. The day we can fully trust AI to write production code unsupervised is not here yet and maybe further in the future than the hype suggests.\r\n\r\nUse Claude Code with your eyes open. Review everything. Don't hit \"always accept.\" And maybe check your API bill more often than you'd like to.","content_text":"Claude code is a powerful AI toolset that runs right in your terminal. While providing a lot of impressive utility, it also suffers from the issues that arise from similar AI toolings with the addition of an expensive pricing model.\nContext is Important\n\nTo me, the main selling point for Claude Code is its ability to read through your entire codebase; a big shortcoming of many AI workflows is the model only partially understanding an issue due to it not having enough of the project's context and convention to be effective. Claude Code has the ability to access all of the files in the directory where you initiated the session, and can even ask for permission to search through extraneous directories. While potentially helpful, this is also a little sketchy when considering that everything Claude is processing is getting sent over the wire to Anthropic's servers; according to their privacy policy, the data sent over for processing will not be used for LLM training unless the user specifically opts in or the content has been flagged for a Trust \u0026amp; Safety Review. It's also important to be aware that Claude's data retention policy has changed in recent months and will likely continue to change.\nStep by Step\n\nWhen you start a Claude Code session and give it a task to complete, it will usually try to break down the task into steps to complete. Upon starting a 'step', Claude will show you what it wants to do, and ask you for permission to do it. In my experience, I always had three options when presented with Claude's suggestion: accept the suggested action, always accept the suggested action, or do not accept and tell Claude to do something different. I would urge any developer to stick to the first and last option. Take the time to look at Claude's suggestion critically and decide if it seems like the right way to go. Claude is usually logical, but also susceptable to hallucinations. A big advantage of Claude Code living in the terminal, is that it has the ability to run bash commands and verify its own work in lots of ways. This means it can actually run tests to see if the code it wrote compiles, if tests pass, or if a server spun up correctly. This was surprisingly useful when trying it out for my own projects and I found that Claude presented lot of concerns that I also held when working through a problem, and also highlighted potential mistakes and optimizations I had yet to consider. Claude is certainly not foolproof and sometimes it tries to confidently to run a command that makes no sense, or try to write some code that is off track. \nThe Cost Question\n\nClaude Code isn't cheap and charges you based on token usage. Each session burns through API tokens at a rate that can add up fast if you're using it regularly. If Claude is going through one of its episodes, it can end up repeating itself in logical thought loops that burn tokens quickly. For hobbyists or developers at smaller companies, this could create a weird calculus where you'd constantly ask if the task you're about to delegate is worth it or not. This friction fundamentally changes how you interact with the tool instead of treating it as a seamless assistant, you're rationing its use (Which is probably a smart thing to do with AI anyways😬).\nWhen It Works (And When It Doesn't)\n\nClaude Code is pretty impressive when working on well defined problems in codebases with obvious patterns and structure (Like a nice Ruby on Rails app). Need to refactor some logic in a Rails model or service object? Want to add some error handling across several files? Need to write some specs for new features? These are tasks where I found Claude Code to have the most success. It typically understands the scope, stays consistent, and navigates the files without losing its train of thought.\n\nBut it struggles with ambiguity. Vague requests like \"make this faster\" or \"fix the bugs\" often lead to Claude making assumptions that don't align with what you actually meant. It also tends to be overly conservative sometimes and overly aggressive other times there's no middle ground. It might refuse to modify a file because it's \"not sure\" what you want, or it might rewrite an entire module when you just wanted a small tweak.\nMy Verdict\n\nClaude Code is a pretty cool tool that improves certain workflows, but it's not a perfect advance in an AI revolution. It's expensive, it requires diligent supervision, and can make some pretty baffling mistakes. For the right tasks, particularly refactoring, boilerplate generation, or getting you out of being stumped by a bug, it can save a significant amount of time. Don't expect to be able to hand Claude Code your project directory and walk away. Claude Code is a tool that is more of a supplement to your workflow than it is a replacement. And for me, that's probably how it should be. The day we can fully trust AI to write production code unsupervised is not here yet and maybe further in the future than the hype suggests.\n\nUse Claude Code with your eyes open. Review everything. Don't hit \"always accept.\" And maybe check your API bill more often than you'd like to.\n","summary":"Claude code is a powerful AI toolset that runs right in your terminal. While providing a lot of impressive utility, it also suffers from the issues that arise from similar AI toolings with the addition of an expensive pricing model.\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1068/Screenshot_2025-11-07_at_5.26.32_PM.png","date_published":"2025-11-18T09:00:00-05:00","data_modified":"2025-11-07T17:27:02-05:00","author":{"name":"Jack Rosa","url":"https://hashrocket.com/team/jackrosa","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/112/headshot.jpg"},"tags":["Ruby","Workflow","rails","AI"]},{"id":"https://hashrocket.com/blog/posts/nativewind-speeding-up-styling-in-react-native","url":"https://hashrocket.com/blog/posts/nativewind-speeding-up-styling-in-react-native","title":"Nativewind: Speeding up Styling in React Native","content_html":"How Nativewind can speed up your React Native Development\n\n\r\nIf you're anything like me, after working on a few web projects with Tailwind, it can feel like a drag to return to stacks that use other styling libraries. Tailwind has become, for myself, and many other developers, a standard styling paradigm. When starting my most recent React Native project, I was relieved to find out that NativeWind exists. NativeWind is exactly what is sounds like: Tailwind Classes in React Native. I can attest to the breeziness of writing an entire native app without a single call to StyleSheet.create.\r\n\r\n## It's Familiar\r\nNativewind takes the familiar classes of Tailwind CSS directly to your React Native components. Virtually every* class from Tailwind CSS can be used the exact same way on your mobile app (and the web), and the development results end up being faster iteration, less translating, and components that feel more readable and maintainable.\r\n\r\nLet’s look at how to set it up\r\n\r\nNativeWind translates Tailwind class names into React Native styles at runtime or compile-time, if you use the Babel plugin. You end up with the the same composable mindset of Tailwind, but the output is just React Native styles.\r\n\r\nThe setup is simple enough, for my latest project we used expo:\r\n\r\n```\r\nnpx expo install nativewind react-native-reanimated@~3.17.4 react-native-safe-area-context@5.4.0\r\nnpx expo install --dev tailwindcss@^3.4.17 prettier-plugin-tailwindcss@^0.5.11\r\n```\r\n\r\nNow to generate a tailwind config, run:\r\n\r\n```\r\nnpx tailwindcss init\r\n```\r\n\r\nBe sure to include the path to your components in the generated tailwind.config.js file\r\n\r\n```javascript\r\n/** @type {import('tailwindcss').Config} */\r\nmodule.exports = {\r\n  content: [\"./App.tsx\", \"./components/**/*.{js,jsx,ts,tsx}\"],\r\n  presets: [require(\"nativewind/preset\")],\r\n  theme: {\r\n    extend: {},\r\n  },\r\n  plugins: [],\r\n}\r\n```\r\n\r\nNext create your global.css file with tailwind's directives\r\n\r\n```css\r\n@tailwind base;\r\n@tailwind components;\r\n@tailwind utilities;\r\n```\r\n\r\nThen, enable the Babel plugin in babel.config.js:\r\n\r\n```jsx\r\nmodule.exports = function (api) {\r\n  api.cache(true);\r\n  return {\r\n    presets: [\r\n      [\"babel-preset-expo\", { jsxImportSource: \"nativewind\" }],\r\n      \"nativewind/babel\",\r\n    ],\r\n  };\r\n};\r\n```\r\n\r\n\r\nNow you’re ready to use all the classes Nativewind provides in your components:\r\n\r\n```jsx\r\nimport { View, Text, Pressable } from 'react-native'\r\n\r\nexport default function MyComponent() {\r\n  return (\r\n    \u003cView className=\"flex-1 items-center justify-center bg-gray-100\"\u003e\r\n      \u003cText className=\"text-2xl font-semibold text-gray-900 mb-4\"\u003e\r\n        Hello world, this component is using NativeWind!\r\n      \u003c/Text\u003e\r\n      \u003cPressable className=\"bg-blue-400 px-4 py-2 rounded-lg active:bg-blue-600\"\u003e\r\n        \u003cText className=\"text-white font-medium\"\u003ePress me\u003c/Text\u003e\r\n      \u003c/Pressable\u003e\r\n    \u003c/View\u003e\r\n  )\r\n}\r\n```\r\n\r\nYou will probably want to create some custom tailwind colors/classes, which you can include into the existing theme.\r\n\r\n```jsx\r\n//tailwind.config.js\r\n\r\nimport { colors } from \"./theme/colors\"\r\nmodule.exports = {\r\n  theme: {\r\n    extend: {\r\n      colors: colors\r\n      },\r\n    },\r\n  },\r\n}\r\n```\r\n\r\nFor more information on installing Nativewind, be sure to check out the [documentation](https://www.nativewind.dev/docs/getting-started/installation)\r\n\r\nDefining style objects in React Native can spiral, and to be fair, so can inline tailwind classes at times. The separation of style sheets can look neat at first, but over time, it can become another layer to maintain, a long list of key names that rarely get reused across files when not carefully organized.\r\n\r\nLet's compare StyleSheets with Nativewind\r\n\r\n```jsx\r\nconst styles = StyleSheet.create({\r\n  container: {\r\n    flex: 1,\r\n    backgroundColor: '#f9fafb',\r\n    justifyContent: 'center',\r\n    alignItems: 'center',\r\n  },\r\n})\r\n```\r\n\r\n\r\nNow compare that to this:\r\n\r\n```jsx\r\n\u003cView className=\"flex-1 bg-gray-50 justify-center items-center\" /\u003e\r\n```\r\n\r\n\r\nThe result is the same, and I'd argue the tailwind is more readable.\r\n\r\nI prefer never having to context switch between the code I'm writing and the styles it uses. Navigating to the bottom of the file just to tweak a component's padding can be tedious.\r\n\r\nAs a developer that uses Tailwind on the web, Nativewind makes me feel right at home. It also means that If you plan on distributing your native app to the web, it's simple to use Tailwind breakpoints to alter the design of your components for larger screens.\r\n\r\n## Integration\r\n\r\nNativewind also plays well with other libraries. Since the generated output is just standard React Native styles, you can still use other animation and navigation libraries. Essentially, if it takes a className prop, you can throw Nativewind styles at it.\r\n\r\n```\r\nimport { MotiView } from 'moti'\r\n\r\n\u003cMotiView\r\n  from={{ opacity: 0, translateY: 8 }}\r\n  animate={{ opacity: 1, translateY: 0 }}\r\n  className=\"p-4 bg-brand-500 rounded-xl\"\r\n/\u003e\r\n```\r\n\r\nAnother great feature is that the basic animation classes included with Tailwind work with Nativewind. At the time of writing this the support is considered experimental, however in my experience all of the ```animate-``` classes work as expected on IOS and Android.\r\n\r\n\r\n## In Conclusion\r\n\r\nThe familiar Tailwind class syntax and easily achieved responsiveness make Nativewind an effective library for speeding up react native development.\r\n\r\nIf you’re tired of jumping back and forth from component renders and StyleSheets, I highly recommend seeing if Nativewind can improve your workflow on your next React Native Project.\r\n\r\n## Work with us\r\n\r\nNeed help with your next React Native project? Don't hesitate to reach out with any questions.","content_text":"How Nativewind can speed up your React Native Development\n\nIf you're anything like me, after working on a few web projects with Tailwind, it can feel like a drag to return to stacks that use other styling libraries. Tailwind has become, for myself, and many other developers, a standard styling paradigm. When starting my most recent React Native project, I was relieved to find out that NativeWind exists. NativeWind is exactly what is sounds like: Tailwind Classes in React Native. I can attest to the breeziness of writing an entire native app without a single call to StyleSheet.create.\nIt's Familiar\n\nNativewind takes the familiar classes of Tailwind CSS directly to your React Native components. Virtually every* class from Tailwind CSS can be used the exact same way on your mobile app (and the web), and the development results end up being faster iteration, less translating, and components that feel more readable and maintainable.\n\nLet’s look at how to set it up\n\nNativeWind translates Tailwind class names into React Native styles at runtime or compile-time, if you use the Babel plugin. You end up with the the same composable mindset of Tailwind, but the output is just React Native styles.\n\nThe setup is simple enough, for my latest project we used expo:\nnpx expo install nativewind react-native-reanimated@~3.17.4 react-native-safe-area-context@5.4.0\nnpx expo install --dev tailwindcss@^3.4.17 prettier-plugin-tailwindcss@^0.5.11\n\nNow to generate a tailwind config, run:\nnpx tailwindcss init\n\nBe sure to include the path to your components in the generated tailwind.config.js file\n/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"./App.tsx\", \"./components/**/*.{js,jsx,ts,tsx}\"],\n  presets: [require(\"nativewind/preset\")],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n}\n\nNext create your global.css file with tailwind's directives\n@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nThen, enable the Babel plugin in babel.config.js:\nmodule.exports = function (api) {\n  api.cache(true);\n  return {\n    presets: [\n      [\"babel-preset-expo\", { jsxImportSource: \"nativewind\" }],\n      \"nativewind/babel\",\n    ],\n  };\n};\n\nNow you’re ready to use all the classes Nativewind provides in your components:\nimport { View, Text, Pressable } from 'react-native'\n\nexport default function MyComponent() {\n  return (\n    \u0026lt;View className=\"flex-1 items-center justify-center bg-gray-100\"\u0026gt;\n      \u0026lt;Text className=\"text-2xl font-semibold text-gray-900 mb-4\"\u0026gt;\n        Hello world, this component is using NativeWind!\n      \u0026lt;/Text\u0026gt;\n      \u0026lt;Pressable className=\"bg-blue-400 px-4 py-2 rounded-lg active:bg-blue-600\"\u0026gt;\n        \u0026lt;Text className=\"text-white font-medium\"\u0026gt;Press me\u0026lt;/Text\u0026gt;\n      \u0026lt;/Pressable\u0026gt;\n    \u0026lt;/View\u0026gt;\n  )\n}\n\nYou will probably want to create some custom tailwind colors/classes, which you can include into the existing theme.\n//tailwind.config.js\n\nimport { colors } from \"./theme/colors\"\nmodule.exports = {\n  theme: {\n    extend: {\n      colors: colors\n      },\n    },\n  },\n}\n\nFor more information on installing Nativewind, be sure to check out the documentation\n\nDefining style objects in React Native can spiral, and to be fair, so can inline tailwind classes at times. The separation of style sheets can look neat at first, but over time, it can become another layer to maintain, a long list of key names that rarely get reused across files when not carefully organized.\n\nLet's compare StyleSheets with Nativewind\nconst styles = StyleSheet.create({\n  container: {\n    flex: 1,\n    backgroundColor: '#f9fafb',\n    justifyContent: 'center',\n    alignItems: 'center',\n  },\n})\n\nNow compare that to this:\n\u0026lt;View className=\"flex-1 bg-gray-50 justify-center items-center\" /\u0026gt;\n\nThe result is the same, and I'd argue the tailwind is more readable.\n\nI prefer never having to context switch between the code I'm writing and the styles it uses. Navigating to the bottom of the file just to tweak a component's padding can be tedious.\n\nAs a developer that uses Tailwind on the web, Nativewind makes me feel right at home. It also means that If you plan on distributing your native app to the web, it's simple to use Tailwind breakpoints to alter the design of your components for larger screens.\nIntegration\n\nNativewind also plays well with other libraries. Since the generated output is just standard React Native styles, you can still use other animation and navigation libraries. Essentially, if it takes a className prop, you can throw Nativewind styles at it.\nimport { MotiView } from 'moti'\n\n\u0026lt;MotiView\n  from={{ opacity: 0, translateY: 8 }}\n  animate={{ opacity: 1, translateY: 0 }}\n  className=\"p-4 bg-brand-500 rounded-xl\"\n/\u0026gt;\n\nAnother great feature is that the basic animation classes included with Tailwind work with Nativewind. At the time of writing this the support is considered experimental, however in my experience all of the animate- classes work as expected on IOS and Android.\nIn Conclusion\n\nThe familiar Tailwind class syntax and easily achieved responsiveness make Nativewind an effective library for speeding up react native development.\n\nIf you’re tired of jumping back and forth from component renders and StyleSheets, I highly recommend seeing if Nativewind can improve your workflow on your next React Native Project.\nWork with us\n\nNeed help with your next React Native project? Don't hesitate to reach out with any questions.\n","summary":"How Nativewind can speed up your React Native Development\n","image":"https://dkj231ikyz7c1.cloudfront.net/uploads/blog/post/image/1035/ChatGPT_Image_Oct_31__2025__06_39_05_PM%282%29.png","date_published":"2025-11-13T09:00:00-05:00","data_modified":"2025-10-31T18:43:52-04:00","author":{"name":"Jack Rosa","url":"https://hashrocket.com/team/jackrosa","avatar":"https://dkj231ikyz7c1.cloudfront.net/uploads/rocketeer/profile_image/112/headshot.jpg"},"tags":["Mobile","React Native"]}]}