All about coding

Ruby and Rails technical content written by Lucian Ghinda

Ruby on Rails Open Source: Mastodon

#ruby #ruby-on-rails #opensource #programming-blogs #programming-languages

Exploring Mastodon: An Open Source Ruby on Rails Web Application

The product

https://joinmastodon.org

Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, and video. All Mastodon servers are interoperable as a federated network.

a social networking site with a purple background

Open source

The project is open source at https://github.com/mastodon/mastodon

License

The license is GNU Affero General Public License v3.0

A screenshot of the https://github.com/mastodon/mastodon/blob/main/LICENSE

Technical review

Architecture

Ruby on Rails - used for API and some of the web pages

React and Redux - used for dynamic parts of the interface

Node.js - used for streaming API

I will focus on this review on the Ruby on Rails part.

Ruby and Rails version

It uses Ruby 3.2.2 and (at the moment of writing this review) Rails 7.1.1

Stats

Running bin/rails stats will give the following:

Running the VScodeCounter will give the following stats:

There are very few actual XML files. So what is marked there as XML is probably a flavour of HTML either from the Ruby on Rails side or from the React/Nodejs side.

Style Guide

They are using Rubocop to enforce coding guidelines. They are using along with standard Rubocop:

And a custom cop called MiddleDot that does the following:

Bans the usage of “•” (bullet) in HTML/HAML in favour of “·” (middle dot) in string literals

Here are some extra configurations for Rubocop:

What I noticed and liked is that for each Rubocop rule where they overwrite, they provide as a comment the URL for that rule. This makes it easier to navigate to it and read about it.

Storage Persistence and in-memory storage

The database used is PostgreSQL and they configure by default a primary and replica DB:

The Database pool is set by using the following:

# https://github.com/mastodon/mastodon/blob/main/config/database.yml#L3
pool: <%= ENV["DB_POOL"] || (if Sidekiq.server? then Sidekiq[:concurrency] else ENV['MAX_THREADS'] end) || 5 %>

So if there is not DB_POOL environment it will try to use Sidekiq[:concurrency] else it will default to `MAX_THREADS`

As in-memory storage it uses Redis and there is a RedisConfiguration object that helps with configuring the connection pool:

# https://github.com/mastodon/mastodon/blob/main/app/lib/redis_configuration.rb#L3

class RedisConfiguration
  class << self
    def establish_pool(new_pool_size)
      @pool&.shutdown(&:close)
      @pool = ConnectionPool.new(size: new_pool_size) { new.connection }
    end

    delegate :with, to: :pool

    def pool
      @pool ||= establish_pool(pool_size)
    end

    def pool_size
      if Sidekiq.server?
        Sidekiq[:concurrency]
      else
        ENV['MAX_THREADS'] || 5
      end
    end
  end

  def connection
    if namespace?
      Redis::Namespace.new(namespace, redis: raw_connection)
    else
      raw_connection
    end
  end
# ... other method
end

It uses a class instance variables @pool to share the connection pool across multiple instances of this class and the size is by default Sidekiq[:concurrency] size if Sidekiq used or MAX_THREADS value.

If there is an environment variable REDIS_NAMESPACE it will use Redis under that specific namespace.

Further down this RedisConfiguration is added to other objects (controllers for example) by using a module called Redisable

# https://github.com/mastodon/mastodon/blob/main/app/models/concerns/redisable.rb#L3

module Redisable
  def redis
    Thread.current[:redis] ||= RedisConfiguration.pool.checkout
  end

  def with_redis(&block)
    RedisConfiguration.with(&block)
  end
end

There is then a module called Lockable that defines with_redis_lock:

# https://github.com/mastodon/mastodon/blob/main/app/models/concerns/lockable.rb#L3

module Lockable
  # @param [String] lock_name
  # @param [ActiveSupport::Duration] autorelease Automatically release the lock after this time
  # @param [Boolean] raise_on_failure Raise an error if a lock cannot be acquired, or fail silently
  # @raise [Mastodon::RaceConditionError]
  def with_redis_lock(lock_name, autorelease: 15.minutes, raise_on_failure: true)
    with_redis do |redis|
      RedisLock.acquire(redis: redis, key: "lock:#{lock_name}", autorelease: autorelease.seconds) do |lock|
        if lock.acquired?
          yield
        elsif raise_on_failure
          raise Mastodon::RaceConditionError, "Could not acquire lock for #{lock_name}, try again later"
        end
      end
    end
  end
end

They are used for example in a controller:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/api/v1/statuses/reblogs_controller.rb#L3

class Api::V1::Statuses::ReblogsController < Api::V1::Statuses::BaseController
  include Redisable
  include Lockable

  # ... callbacks

  def create
    with_redis_lock("reblog:#{current_account.id}:#{@reblog.id}") do
      @status = ReblogService.new.call(current_account, @reblog, reblog_params)
    end

    render json: @status, serializer: REST::StatusSerializer
  end
  # ... other methods
end

Gems used

I picked some gems from the Gemfile that I found interesting to mention:

Code & Design Patterns

Folders

Here are some non-standard Rails folders:

Controllers

There are 2 main categories of controllers when thinking about how the information will be displayed:

Caching

It implements the Vary header:

The Vary HTTP response header describes the parts of the request message aside from the method and URL that influenced the content of the response it occurs in. Most often, this is used to create a cache key when content negotiation is in use.

# https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/cache_concern.rb#L150
def vary_by(value, **kwargs)
  before_action(**kwargs) do |controller|
    response.headers['Vary'] = value.respond_to?(:call) ? controller.instance_exec(&value) : value
  end
end

# Used like this

# https://github.com/mastodon/mastodon/blob/main/app/controllers/follower_accounts_controller.rb#L3
vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' }

A note about using instance_exec is a method from BasicObject#instance_eval that can be used to execute a block within the context of the receiver.

Another method used is render_with_cache used by API controllers to cache JSON responses:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/cache_concern.rb#L172 

def render_with_cache(**options)
  raise ArgumentError, 'Only JSON render calls are supported' unless options.key?(:json) || block_given?

  key        = options.delete(:key) || [[params[:controller], params[:action]].join('/'), options[:json].respond_to?(:cache_key) ? options[:json].cache_key : nil, options[:fields].nil? ? nil : options[:fields].join(',')].compact.join(':')
  expires_in = options.delete(:expires_in) || 3.minutes
  body       = Rails.cache.read(key, raw: true)

  if body
    render(options.except(:json, :serializer, :each_serializer, :adapter, :fields).merge(json: body))
  else
    if block_given?
      options[:json] = yield
    elsif options[:json].is_a?(Symbol)
      options[:json] = send(options[:json])
    end

    render(options)
    Rails.cache.write(key, response.body, expires_in: expires_in, raw: true)
  end
end

# Used like this:
# https://github.com/mastodon/mastodon/blob/main/app/controllers/accounts_controller.rb#L35

format.json do
  expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
  render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
end

And a third method that is used is cache_collection used with models that include the Cacheable concern:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/concerns/cache_concern.rb#L193-L194

def cache_collection(raw, klass)
  return raw unless klass.respond_to?(:with_includes)

  raw = raw.cache_ids.to_a if raw.is_a?(ActiveRecord::Relation)
  return [] if raw.empty?

  cached_keys_with_value = begin
    Rails.cache.read_multi(*raw).transform_keys(&:id).transform_values { |r| ActiveRecordCoder.load(r) }
  rescue ActiveRecordCoder::Error
    {} # The serialization format may have changed, let's pretend it's a cache miss.
  end

  uncached_ids = raw.map(&:id) - cached_keys_with_value.keys

  klass.reload_stale_associations!(cached_keys_with_value.values) if klass.respond_to?(:reload_stale_associations!)

  unless uncached_ids.empty?
    uncached = klass.where(id: uncached_ids).with_includes.index_by(&:id)

    uncached.each_value do |item|
      Rails.cache.write(item, ActiveRecordCoder.dump(item))
    end
  end

  raw.filter_map { |item| cached_keys_with_value[item.id] || uncached[item.id] }
end

# Used like this:

format.rss do
  expires_in 1.minute, public: true

  limit     = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
  @statuses = filtered_statuses.without_reblogs.limit(limit)
  @statuses = cache_collection(@statuses, Status)
end

Controller with multiple respond_to

Here is an example of a controller action with multiple respond_to formats:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/accounts_controller.rb#L17
def show
  respond_to do |format|
    format.html do
      expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?

      @rss_url = rss_url
    end

    format.rss do
      expires_in 1.minute, public: true

      limit     = params[:limit].present? ? [params[:limit].to_i, PAGE_SIZE_MAX].min : PAGE_SIZE
      @statuses = filtered_statuses.without_reblogs.limit(limit)
      @statuses = cache_collection(@statuses, Status)
    end

    format.json do
      expires_in 3.minutes, public: !(authorized_fetch_mode? && signed_request_account.present?)
      render_with_cache json: @account, content_type: 'application/activity+json', serializer: ActivityPub::ActorSerializer, adapter: ActivityPub::Adapter
    end
  end
end

ApplicationController

It includes a list of concerns, defines some helper methods and define a couple of rescue_from responses.

What I discovered here and I find useful to have a common language when writing custom controllers:

truthy_param? the method that I think is useful to use to check if a param is a truthy value:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/application_controller.rb#L91
def truthy_param?(key)
  ActiveModel::Type::Boolean.new.cast(params[key])
end

# Used like this

# https://github.com/mastodon/mastodon/blob/main/app/controllers/api/v1/timelines/public_controller.rb#L37-L44
def public_feed
  PublicFeed.new(
    current_account,
    local: truthy_param?(:local),
    remote: truthy_param?(:remote),
    only_media: truthy_param?(:only_media)
  )
end

A series of methods that can be used to respond with errors:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/application_controller.rb#L169-L174

# First there is a respond_with_error defined: 
def respond_with_error(code)
  respond_to do |format|
    format.any  { render "errors/#{code}", layout: 'error', status: code, formats: [:html] }
    format.json { render json: { error: Rack::Utils::HTTP_STATUS_CODES[code] }, status: code }
  end
end

# Then there a series of methods defined like this:

# https://github.com/mastodon/mastodon/blob/main/app/controllers/application_controller.rb#L96
def forbidden
  respond_with_error(403)
end

def not_found
  respond_with_error(404)
end

def gone
  respond_with_error(410)
end

Models

Some stats:

The ApplicationRecord model is connected to writing and reading databases and defines one common instance method boolean_with_default that will be used to define some getters that return a default value when nil. It is a simple method but very useful.

# https://github.com/mastodon/mastodon/blob/main/app/models/application_record.rb#L16

def boolean_with_default(key, default_value)
  value = attributes[key]

  if value.nil?
    default_value
  else
    value
  end
end

# Used like this

# https://github.com/mastodon/mastodon/blob/main/app/models/tag.rb#L71

def usable
  boolean_with_default('usable', true)
end

alias usable? usable

Also, notice the choice to use alias to define the predicate method usable? instead of defining it as a normal method (e.g. def usable? = usable)

Here are some things that I found in the models:

Counting cache

The Account model has a couple of associated stats models implemented by using a has_one :account_stat that includes some colums like: statuses_count, following_count, followers_count . The count is incremented by executing the following SQL:

# https://github.com/mastodon/mastodon/blob/main/app/models/concerns/account_counters.rb#L34

def update_count!(key, value)
# omitted code ...

  sql = if value.positive? && key == :statuses_count
          <<-SQL.squish
            INSERT INTO account_stats(account_id, #{key}, created_at, updated_at, last_status_at)
              VALUES (:account_id, :default_value, now(), now(), now())
            ON CONFLICT (account_id) DO UPDATE
            SET #{key} = account_stats.#{key} + :value,
                last_status_at = now(),
                updated_at = now()
            RETURNING id;
          SQL
        else
          <<-SQL.squish
            INSERT INTO account_stats(account_id, #{key}, created_at, updated_at)
              VALUES (:account_id, :default_value, now(), now())
            ON CONFLICT (account_id) DO UPDATE
            SET #{key} = account_stats.#{key} + :value,
                updated_at = now()
            RETURNING id;
          SQL
        end

  sql = AccountStat.sanitize_sql([sql, account_id: id, default_value: default_value, value: value])
  account_stat_id = AccountStat.connection.exec_query(sql)[0]['id']
  # Omitted code ..
end

# Used like this

def increment_count!(key) = update_count!(key, 1)
def decrement_count!(key) = update_count!(key, -1)

The reason for using the custom SQL is explained in the same method:

We do an upsert using manually written SQL, as Rails’ upsert method does not seem to support writing expressions in the UPDATE clause, but only re-insert the provided values instead. Even ARel seem to be missing proper handling of upserts.

This will be used in the following way:

class Favourite < ApplicationRecord
  after_create :increment_cache_counters
  belongs_to :status,  inverse_of: :favourites
  # Code omitted ... 
  private

  def increment_cache_counters
    status&.increment_count!(:favourites_count)
  end
  # code omitted ...
end

User Roles

User Roles are defined in the UserRole AR model using the bitwise shift left operator:

class UserRole < ApplicationRecord
  FLAGS = {
    administrator: (1 << 0),
    view_devops: (1 << 1),
    view_audit_log: (1 << 2),
    view_dashboard: (1 << 3),
    manage_reports: (1 << 4),
    manage_federation: (1 << 5),
    manage_settings: (1 << 6),
    manage_blocks: (1 << 7),
    manage_taxonomies: (1 << 8),
    manage_appeals: (1 << 9),
    manage_users: (1 << 10),
    manage_invites: (1 << 11),
    manage_rules: (1 << 12),
    manage_announcements: (1 << 13),
    manage_custom_emojis: (1 << 14),
    manage_webhooks: (1 << 15),
    invite_users: (1 << 16),
    manage_roles: (1 << 17),
    manage_user_access: (1 << 18),
    delete_user_data: (1 << 19),
  }.freeze

  module Flags
    NONE = 0
    ALL  = FLAGS.values.reduce(&:|)

    DEFAULT = FLAGS[:invite_users]
    # code omitted ... 
  end

  # code omitted ... 
end

In case you want to understand how they are used, here are some expressions:

Then in the UserRole model there are the following methods defined:

# https://github.com/mastodon/mastodon/blob/main/app/models/user_role.rb#L164

def can?(*any_of_privileges)
  any_of_privileges.any? { |privilege| in_permissions?(privilege) }
end

def in_permissions?(privilege)
  raise ArgumentError, "Unknown privilege: #{privilege}" unless FLAGS.key?(privilege)

  computed_permissions & FLAGS[privilege] == FLAGS[privilege]
end

They are used in AccountPolicy like this:

# https://github.com/mastodon/mastodon/blob/main/app/policies/account_policy.rb#L3

class AccountPolicy < ApplicationPolicy
  def index?
    role.can?(:manage_users)
  end

  def show?
    role.can?(:manage_users)
  end
# code omitted ...
end

Services

BaseService

There is a simple BaseService that looks like this:

# https://github.com/mastodon/mastodon/blob/main/app/services/base_service.rb#L3
class BaseService
  include ActionView::Helpers::TextHelper
  include ActionView::Helpers::SanitizeHelper

  include RoutingHelper

  def call(*)
    raise NotImplementedError
  end
end

and then almost all services are inherited from this BaseService

The services defined here can be called from controllers or from models.

Here is an example of the Follow service (will just add here the #call method):

# https://github.com/mastodon/mastodon/blob/main/app/services/follow_service.rb#L3

# Follow a remote user, notify remote user about the follow
# @param [Account] source_account From which to follow
# @param [Account] target_account Account to follow
# @param [Hash] options
# @option [Boolean] :reblogs Whether or not to show reblogs, defaults to true
# @option [Boolean] :notify Whether to create notifications about new posts, defaults to false
# @option [Array<String>] :languages Which languages to allow on the home feed from this account, defaults to all
# @option [Boolean] :bypass_locked
# @option [Boolean] :bypass_limit Allow following past the total follow number
# @option [Boolean] :with_rate_limit
def call(source_account, target_account, options = {})
  @source_account = source_account
  @target_account = target_account
  @options        = { bypass_locked: false, bypass_limit: false, with_rate_limit: false }.merge(options)

  raise ActiveRecord::RecordNotFound if following_not_possible?
  raise Mastodon::NotPermittedError  if following_not_allowed?

  if @source_account.following?(@target_account)
    return change_follow_options!
  elsif @source_account.requested?(@target_account)
    return change_follow_request_options!
  end

  ActivityTracker.increment('activity:interactions')

  # When an account follows someone for the first time, avoid showing
  # an empty home feed while the follow request is being processed
  # and the feeds are being merged
  mark_home_feed_as_partial! if @source_account.not_following_anyone?

  if (@target_account.locked? && !@options[:bypass_locked]) || @source_account.silenced? || @target_account.activitypub?
    request_follow!
  elsif @target_account.local?
    direct_follow!
  end
end

FetchLinkCardService

Here is a piece of code from FetchLinkCardService that parses a status and extracts shared URLs.

First, it defines a Regexp for searching for URL patterns:

# https://github.com/mastodon/mastodon/blob/main/app/services/fetch_link_card_service.rb#L7
  URL_PATTERN = %r{
    (#{Twitter::TwitterText::Regex[:valid_url_preceding_chars]})                                                                #   $1 preceding chars
    (                                                                                                                           #   $2 URL
      (https?://)                                                                                                               #   $3 Protocol (required)
      (#{Twitter::TwitterText::Regex[:valid_domain]})                                                                           #   $4 Domain(s)
      (?::(#{Twitter::TwitterText::Regex[:valid_port_number]}))?                                                                #   $5 Port number (optional)
      (/#{Twitter::TwitterText::Regex[:valid_url_path]}*)?                                                                      #   $6 URL Path and anchor
      (\?#{Twitter::TwitterText::Regex[:valid_url_query_chars]}*#{Twitter::TwitterText::Regex[:valid_url_query_ending_chars]})? #   $7 Query String
    )
  }iox

Then it extracts URLs with:

# https://github.com/mastodon/mastodon/blob/main/app/services/fetch_link_card_service.rb#L73
def parse_urls
  urls = if @status.local?
            @status.text.scan(URL_PATTERN).map { |array| Addressable::URI.parse(array[1]).normalize }
          else
            document = Nokogiri::HTML(@status.text)
            links = document.css('a')

            links.filter_map { |a| Addressable::URI.parse(a['href']) unless skip_link?(a) }.filter_map(&:normalize)
          end

  urls.reject { |uri| bad_url?(uri) }.first
end

TagSearchService

Here is a nice and easy-to-read method in TagSearchService

# https://github.com/mastodon/mastodon/blob/main/app/services/tag_search_service.rb#L4

def call(query, options = {})
  @query   = query.strip.delete_prefix('#')
  @offset  = options.delete(:offset).to_i
  @limit   = options.delete(:limit).to_i
  @options = options

  results   = from_elasticsearch if Chewy.enabled?
  results ||= from_database

  results
end

Views

The views have the following structure:

Example from the Home#index:

- # https://github.com/mastodon/mastodon/blob/main/app/views/home/index.html.haml#L1
- content_for :header_tags do
  - unless request.path == '/'
    %meta{ name: 'robots', content: 'noindex' }/

  = render partial: 'shared/og'

= render 'shared/web_app'

Where the web_app will do something like this:

I will not go into this review into the Javascript part as this is focused on Ruby and Rails.

Testing

It uses RSPec for testing.

In general, the tests are pretty straightforward, with very few custom matches defined.

The tests are easy to find as the folder inside spec matches the folder in app.

The coverage is a bit hard to guess from the test folder structure:

Just looking at some numbers (as I don’t have time to understand the entire codebase) here is a comparison of the number of files in the app folder vs the number of files in the corresponding specs folder.

Generating data

It uses Fabricator gem to generate test data. Here is an example of generating a User:

Fabricator(:user) do
  account      { Fabricate.build(:account, user: nil) }
  email        { sequence(:email) { |i| "#{i}#{Faker::Internet.email}" } }
  password     '123456789'
  confirmed_at { Time.zone.now }
  current_sign_in_at { Time.zone.now }
  agreement true
end

and then it can be called as Fabricator(:user)

Testing views

Probably the React part is tested in some other specific way. I will not dig into that. But here is a test for open graph tags included in the status view:

# https://github.com/mastodon/mastodon/blob/main/spec/views/statuses/show.html.haml_spec.rb#L5
describe 'statuses/show.html.haml', :without_verify_partial_doubles do
  before do
    allow(view).to receive_messages(api_oembed_url: '', show_landing_strip?: true, site_title: 'example site', site_hostname: 'example.com', full_asset_url: '//asset.host/image.svg', current_account: nil, single_user_mode?: false)
    allow(view).to receive(:local_time)
    allow(view).to receive(:local_time_ago)
    assign(:instance_presenter, InstancePresenter.new)
  end

  it 'has valid opengraph tags' do
    alice  = Fabricate(:account, username: 'alice', display_name: 'Alice')
    status = Fabricate(:status, account: alice, text: 'Hello World')
    Fabricate(:media_attachment, account: alice, status: status, type: :video)

    assign(:status, status)
    assign(:account, alice)
    assign(:descendant_threads, [])

    render

    header_tags = view.content_for(:header_tags)

    expect(header_tags).to match(/<meta content=".+" property="og:title">/)
    expect(header_tags).to match(/<meta content="article" property="og:type">/)
    expect(header_tags).to match(/<meta content=".+" property="og:image">/)
    expect(header_tags).to match(%r{<meta content="http://.+" property="og:url">})
  end
# code omitted ...
end

Testing Rack::Attack

This is done via shared_examples:

# https://github.com/mastodon/mastodon/blob/main/spec/config/initializers/rack/attack_spec.rb#L5

describe Rack::Attack, type: :request do
  def app
    Rails.application
  end

  shared_examples 'throttled endpoint' do
    before do
      # Rack::Attack periods are not rolling, so avoid flaky tests by setting the time in a way
      # to avoid crossing period boundaries.

      # The code Rack::Attack uses to set periods is the following:
      # https://github.com/rack/rack-attack/blob/v6.6.1/lib/rack/attack/cache.rb#L64-L66
      # So we want to minimize `Time.now.to_i % period`

      travel_to Time.zone.at(counter_prefix * period.seconds)
    end

    context 'when the number of requests is lower than the limit' do
      before do
        below_limit.times { increment_counter }
      end

      it 'does not change the request status' do
        expect { request.call }.to change { throttle_count }.by(1)

        expect(response).to_not have_http_status(429)
      end
    end

    def throttle_count
      described_class.cache.read("#{counter_prefix}:#{throttle}:#{remote_ip}") || 0
    end
 # code omitted ..
end

and the shared example will be used like this:

context 'when accessed through the website' do
  let(:throttle) { 'throttle_sign_up_attempts/ip' }
  let(:limit)  { 25 }
  let(:period) { 5.minutes }
  let(:request) { -> { post path, headers: { 'REMOTE_ADDR' => remote_ip } } }

  context 'with exact path' do
    let(:path) { '/auth' }

    it_behaves_like 'throttled endpoint'
  end
# code omitted ..
end

Conclusion

Mastodon open-source web app is a well-structured code base, easy to navigate (when focusing on the Ruby on Rails part). There is a good structure for testing and it probably covers most of the important functions, but there are still some models and controllers left out of testing that could be covered.

The code design is simple and easy to understand. The app tried to have slim controllers and pretty well-contained models while delegating more complex business logic to Service objects and simple Ruby objects. This makes the existing tests simple to follow and understand.

I think it could be a good pick for someone who wants to contribute. There are useful pieces of code that can be used as inspiration for other parts.


Enjoyed this article?

👉 Join my Short Ruby News newsletter for weekly Ruby updates from the community and visit rubyandrails.info, a directory with learning content about Ruby.

👐 Subscribe to my Ruby and Ruby on rails courses over email at learn.shortruby.com - effortless learning anytime, anywhere

🤝 Let’s connect on Ruby.social or Linkedin or Twitter where I post mainly about Ruby and Rails.

🎥 Follow me on my YouTube channel for short videos about Ruby

View posts by tag

#ruby #concurrency #general-advice #life-hack #ruby-on-rails #google-cloud #kubernetes #bash #apis #rest-api #guide #docker #bugs-and-errors #howtos #newbie #programming-languages #programming-tips #til #coding #learning #postgresql #sql #design-patterns #web-api #programming-blogs #programmer #databases #programming #learn-coding #learning-journey #side-project #coding-challenge #design-principles #solid-principles #git #tips #testing #test-driven-development #test-automation #code #shortpost #refactoring #code-review #rails #developer #documentation #focus #blogging #content-creation #share #advice #newsletter #active-record #ai #ai-tools #generative-ai #web-development #errors #cursor-ide #ides #gpt-4 #news #scalability #tailwind-css #chatgpt #opensource #reviews #startups #github #technology #types #sorbet #performance #rubymine #vscode #codesnippet #features #syntax #friendlyrb #conference #directories #writing #summary #animation #articles #podcasts #publishing #patterns #pattern-matching #content #updates #seo-for-developers #marketing #properties #gems #matching #rspec #llm #tools #lsp #claudeai #opencode #mcp #rubocop #quality #planning #products #mvp #upgrade #agentic-ai #skills #superpowers