Ruby on Rails sollicitatievragen: Top 25 in 2026
De 25 meest gestelde Ruby on Rails sollicitatievragen. MVC-architectuur, Active Record, migraties, RSpec-testing, REST-APIs met gedetailleerde antwoorden en codevoorbeelden.

Ruby on Rails-sollicitatiegesprekken beoordelen de beheersing van het populairste Ruby-framework, het begrip van de MVC-architectuur, de Active Record-ORM en het vermogen om robuuste webapplicaties te bouwen volgens de filosofie "Convention over Configuration". Deze gids behandelt de 25 meest gestelde vragen, van Rails-grondbeginselen tot geavanceerde productiepatronen.
Recruiters waarderen kandidaten die de Rails-filosofie begrijpen: "Convention over Configuration", DRY (Don't Repeat Yourself) en Rails Way-patronen. Uitleggen waarom Rails bepaalde architecturale keuzes maakt, maakt het verschil.
Ruby on Rails-grondbeginselen
Vraag 1: Leg het MVC-patroon in Ruby on Rails uit
Het Model-View-Controller-patroon (MVC) vormt de architecturale kern van Rails. Het scheidt verantwoordelijkheden in drie afzonderlijke lagen voor betere onderhoudbaarheid en testbaarheid van de code.
# app/models/article.rb
# Het Model beheert de gegevens en de bedrijfslogica
class Article < ApplicationRecord
# Datavalidaties
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
# Associaties met andere modellen
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :tags, through: :article_tags
# Scopes voor herbruikbare queries
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Levenscyclus-callbacks
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if title_changed?
end
end# app/controllers/articles_controller.rb
# De Controller ontvangt verzoeken en orkestreert het antwoord
class ArticlesController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_article, only: [:show, :edit, :update, :destroy]
def index
@articles = Article.published.recent.includes(:author)
end
def show
@comments = @article.comments.includes(:user)
end
def create
@article = current_user.articles.build(article_params)
if @article.save
redirect_to @article, notice: 'Artikel succesvol aangemaakt.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_article
@article = Article.find(params[:id])
end
def article_params
params.require(:article).permit(:title, :body, :published, tag_ids: [])
end
end<%# app/views/articles/show.html.erb %>
<%# De View toont de gegevens in HTML-formaat %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<p class="meta">
Door <%= @article.author.name %> •
<%= l @article.created_at, format: :long %>
</p>
</header>
<div class="content">
<%= simple_format @article.body %>
</div>
<%# Partial voor de reacties %>
<%= render @comments %>
</article>De typische flow: het verzoek komt aan bij de Router, die het naar de juiste Controller stuurt. De Controller communiceert met het Model om gegevens op te halen of te wijzigen, en geeft die gegevens vervolgens door aan de View voor HTML-rendering.
Vraag 2: Wat is Active Record en hoe werkt de Rails-ORM?
Active Record is de ORM (Object-Relational Mapping) van Rails die het Active Record-patroon implementeert. Elke Model-klasse vertegenwoordigt een databasetabel en elke instantie vertegenwoordigt een rij.
# app/models/user.rb
# Active Record koppelt kolommen automatisch aan attributen
class User < ApplicationRecord
# De tabel 'users' wordt automatisch gekoppeld
# Kolommen: id, email, name, created_at, updated_at
has_secure_password # BCrypt voor het wachtwoord
has_many :articles, foreign_key: :author_id
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
# Validaties
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Callbacks
before_save :normalize_email
# Klassemethoden voor queries
def self.admins
joins(:roles).where(roles: { name: 'admin' })
end
private
def normalize_email
self.email = email.downcase.strip
end
end# Voorbeelden van Active Record-queries
# Rails-console of binnen een service
# Aanmaken
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')
# Lezen met voorwaarden
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')
# Geketende queries (lazy evaluation)
recent_admins = User.admins
.where('created_at > ?', 1.month.ago)
.includes(:profile)
.limit(10)
# N+1-preventie met eager loading
articles = Article.includes(:author, :comments).published
# Bijwerken
user.update!(name: 'Alice Martin')
# Transacties
User.transaction do
user.debit_balance!(100)
recipient.credit_balance!(100)
Payment.create!(from: user, to: recipient, amount: 100)
endActive Record vertaalt Ruby-methoden naar geoptimaliseerde SQL-queries. Methoden zoals where, joins en includes zijn lazy: de query wordt pas uitgevoerd bij het itereren of bij het aanroepen van to_a.
Vraag 3: Leg het migratiesysteem van Rails uit
Migraties maken het mogelijk om het databaseschema met Ruby te versioneren. Ze zijn omkeerbaar en stellen een gecontroleerde evolutie van de gegevensstructuur mogelijk.
# db/migrate/20260203100000_create_products.rb
# Migratie om een tabel aan te maken
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :name, null: false
t.text :description
t.decimal :price, precision: 10, scale: 2, null: false
t.integer :stock_quantity, default: 0
t.references :category, null: false, foreign_key: true
t.boolean :active, default: true
t.timestamps # created_at en updated_at automatisch
end
# Indexen voor performance
add_index :products, :name
add_index :products, [:category_id, :active]
end
end# db/migrate/20260203110000_add_slug_to_products.rb
# Migratie om een bestaande tabel te wijzigen
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :slug, :string
add_index :products, :slug, unique: true
# Bestaande slugs vullen
reversible do |dir|
dir.up do
Product.find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
# Niet-nullable maken na het vullen
change_column_null :products, :slug, false
end
end# Essentiële migratiecommando's
rails db:migrate # Openstaande migraties uitvoeren
rails db:rollback # Laatste migratie ongedaan maken
rails db:rollback STEP=3 # Laatste 3 migraties ongedaan maken
rails db:migrate:status # Status van migraties bekijken
rails db:seed # db/seeds.rb uitvoeren
rails db:reset # Drop, create, migrate, seedMigraties moeten omkeerbaar zijn. De methode change is intelligent en kan veelvoorkomende operaties automatisch terugdraaien. Voor complexe gevallen up en down apart gebruiken.
Geavanceerd Active Record
Vraag 4: Hoe N+1-queries optimaliseren in Rails?
Het N+1-probleem treedt op wanneer een initiële query gevolgd wordt door N extra queries om associaties te laden. Rails biedt verschillende eager loading-methoden om dit probleem op te lossen.
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
# ❌ N+1-PROBLEEM: 1 query + N queries per bestelling
# @orders = Order.all
# In de view: order.user.name genereert een query per bestelling
# ✅ OPLOSSING met includes (eager loading)
@orders = Order.includes(:user, :items)
.where(status: 'completed')
.order(created_at: :desc)
# Genereert in totaal slechts 3 queries
end
def show
# includes: laadt associaties apart (2-3 queries)
@order = Order.includes(items: :product).find(params[:id])
# preload: forceert apart laden
@order = Order.preload(:items, :user).find(params[:id])
# eager_load: forceert LEFT OUTER JOIN (1 query)
@order = Order.eager_load(:items).find(params[:id])
end
end# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
has_many :items, class_name: 'OrderItem'
has_many :products, through: :items
# Scope met standaard includes
scope :with_details, -> { includes(:user, items: :product) }
# Counter cache om COUNT-queries te vermijden
# Vereist: add_column :users, :orders_count, :integer, default: 0
belongs_to :user, counter_cache: true
end# N+1-detectie met de Bullet-gem (development)
# config/environments/development.rb
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.rails_logger = true
end
# Bullet toont waarschuwingen wanneer:
# - Een N+1-query wordt gedetecteerd
# - Onnodige eager loading aanwezig is
# - Een counter cache gebruikt zou moeten wordenDe regel: standaard includes gebruiken (Rails kiest de optimale strategie), preload wanneer aparte queries gewenst zijn, eager_load wanneer er op associaties gefilterd wordt.
Vraag 5: Leg Scopes en Query Objects in Rails uit
Scopes kapselen herbruikbare query-voorwaarden in. Voor complexe queries bieden Query Objects een betere organisatie en testbaarheid.
# app/models/product.rb
class Product < ApplicationRecord
# Eenvoudige scopes
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :featured, -> { where(featured: true) }
# Scopes met parameters
scope :cheaper_than, ->(price) { where('price < ?', price) }
scope :in_category, ->(category) { where(category: category) }
# Ketenbare scopes
scope :available, -> { active.in_stock }
# Scope met joins
scope :with_recent_orders, -> {
joins(:order_items)
.where('order_items.created_at > ?', 30.days.ago)
.distinct
}
# Scope met subquery
scope :bestsellers, -> {
where(id: OrderItem.group(:product_id)
.order('COUNT(*) DESC')
.limit(10)
.select(:product_id))
}
end# app/queries/products_search_query.rb
# Query Object voor complexe zoekopdrachten
class ProductsSearchQuery
def initialize(relation = Product.all)
@relation = relation
end
def call(params)
@relation = filter_by_category(params[:category])
@relation = filter_by_price_range(params[:min_price], params[:max_price])
@relation = filter_by_search(params[:q])
@relation = apply_sorting(params[:sort])
@relation
end
private
def filter_by_category(category)
return @relation if category.blank?
@relation.where(category_id: category)
end
def filter_by_price_range(min, max)
@relation = @relation.where('price >= ?', min) if min.present?
@relation = @relation.where('price <= ?', max) if max.present?
@relation
end
def filter_by_search(query)
return @relation if query.blank?
@relation.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def apply_sorting(sort)
case sort
when 'price_asc' then @relation.order(price: :asc)
when 'price_desc' then @relation.order(price: :desc)
when 'newest' then @relation.order(created_at: :desc)
else @relation.order(:name)
end
end
end
# Gebruik in de controller
@products = ProductsSearchQuery.new(Product.active).call(params)Scopes zijn perfect voor eenvoudige, herbruikbare voorwaarden. Query Objects passen bij complexe zoekopdrachten met meerdere optionele filters en compositielogica.
Klaar om je Ruby on Rails gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Routing en Controllers
Vraag 6: Hoe werkt RESTful routing in Rails?
Rails moedigt RESTful routes aan die HTTP-werkwoorden koppelen aan CRUD-acties. De router vertaalt URL's naar specifieke controller-aanroepen.
# config/routes.rb
Rails.application.routes.draw do
# Standaard RESTful routes (7 acties)
resources :articles do
# Geneste routes
resources :comments, only: [:create, :destroy]
# Member routes (op een instantie)
member do
post :publish
delete :archive
end
# Collection routes (op de collectie)
collection do
get :drafts
get :search
end
end
# API-routes met namespace
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show, :create, :update] do
resources :reviews, shallow: true
end
end
end
# Aangepaste route
get 'dashboard', to: 'dashboard#index'
# Routebeperkingen
constraints(SubdomainConstraint.new) do
resources :admin_settings
end
# Hoofdroute
root 'home#index'
end# rails routes - Toont alle gegenereerde routes
#
# Verb URI Pattern Controller#Action
# GET /articles articles#index
# POST /articles articles#create
# GET /articles/new articles#new
# GET /articles/:id/edit articles#edit
# GET /articles/:id articles#show
# PATCH /articles/:id articles#update
# DELETE /articles/:id articles#destroy
# POST /articles/:id/publish articles#publish
# GET /articles/drafts articles#draftsGegenereerde route-helpers (article_path(@article), new_article_path) maken het mogelijk om URL's dynamisch en onderhoudbaar te refereren.
Vraag 7: Leg callbacks en filters in controllers uit
Callbacks (before_action, after_action, around_action) maken het mogelijk om code voor, na of rond controlleracties uit te voeren.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# CSRF-bescherming standaard ingeschakeld
protect_from_forgery with: :exception
# Globale callback voor authenticatie
before_action :authenticate_user!
# Globale foutafhandeling
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render json: { error: 'Resource niet gevonden' }, status: :not_found
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end# app/controllers/admin/products_controller.rb
class Admin::ProductsController < ApplicationController
# Callbacks met opties
before_action :require_admin
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
# Voorwaardelijke callback
before_action :check_stock, only: [:update], if: :stock_changed?
def create
@product = Product.new(product_params)
if @product.save
redirect_to [:admin, @product], notice: 'Product aangemaakt.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @product.update(product_params)
redirect_to [:admin, @product], notice: 'Product bijgewerkt.'
else
render :edit, status: :unprocessable_entity
end
end
private
def require_admin
redirect_to root_path unless current_user&.admin?
end
def set_product
@product = Product.find(params[:id])
end
def stock_changed?
params[:product][:stock_quantity].present?
end
def log_activity
ActivityLog.create!(
user: current_user,
action: action_name,
resource: @product
)
end
def product_params
params.require(:product).permit(:name, :price, :description, :stock_quantity)
end
endCallbacks worden uitgevoerd in volgorde van declaratie. Gebruik skip_before_action in subklassen om geërfde callbacks uit te schakelen. Vermijd callbacks met te veel bedrijfslogica: geef de voorkeur aan Service Objects.
Services en architectuur
Vraag 8: Hoe Service Objects in Rails implementeren?
Service Objects kapselen complexe bedrijfslogica in die niet thuishoort in Models of Controllers. Ze verbeteren de testbaarheid en volgen het Single Responsibility-principe.
# app/services/order_processor.rb
# Service Object met gestandaardiseerde interface
class OrderProcessor
def initialize(order, payment_method:)
@order = order
@payment_method = payment_method
end
def call
return failure('Bestelling al verwerkt') if @order.processed?
ActiveRecord::Base.transaction do
validate_stock!
process_payment!
update_inventory!
send_confirmation!
@order.update!(status: 'completed', processed_at: Time.current)
end
success(@order)
rescue PaymentError => e
failure("Betaling mislukt: #{e.message}")
rescue InsufficientStockError => e
failure("Voorraad onvoldoende: #{e.message}")
end
private
def validate_stock!
@order.items.each do |item|
unless item.product.stock_quantity >= item.quantity
raise InsufficientStockError, item.product.name
end
end
end
def process_payment!
result = PaymentGateway.charge(
amount: @order.total,
method: @payment_method,
description: "Bestelling ##{@order.id}"
)
raise PaymentError, result.error unless result.success?
@order.update!(payment_reference: result.transaction_id)
end
def update_inventory!
@order.items.each do |item|
item.product.decrement!(:stock_quantity, item.quantity)
end
end
def send_confirmation!
OrderMailer.confirmation(@order).deliver_later
end
def success(data)
Result.new(success: true, data: data)
end
def failure(error)
Result.new(success: false, error: error)
end
Result = Struct.new(:success, :data, :error, keyword_init: true) do
def success? = success
def failure? = !success
end
end# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def create
@order = current_user.orders.build(order_params)
if @order.save
result = OrderProcessor.new(@order, payment_method: params[:payment_method]).call
if result.success?
redirect_to @order, notice: 'Bestelling bevestigd!'
else
@order.update!(status: 'payment_failed')
flash.now[:alert] = result.error
render :new, status: :unprocessable_entity
end
else
render :new, status: :unprocessable_entity
end
end
endHet Service Object-patroon volgt een eenvoudige conventie: één klasse, één verantwoordelijkheid, één publieke call-methode. Een Result-object teruggeven maakt een nette afhandeling van succes en mislukking mogelijk.
Vraag 9: Leg Concerns in Rails uit
Concerns maken het mogelijk om code tussen Models of Controllers te extraheren en te delen. Ze gebruiken ActiveSupport::Concern voor een schone include-syntax.
# app/models/concerns/sluggable.rb
# Herbruikbare Concern voor het genereren van slugs
module Sluggable
extend ActiveSupport::Concern
included do
# Code die bij include wordt uitgevoerd
before_validation :generate_slug, if: :should_generate_slug?
validates :slug, presence: true, uniqueness: true
end
# Klassemethoden
class_methods do
def find_by_slug!(slug)
find_by!(slug: slug)
end
def sluggable_source(column = :title)
@sluggable_source = column
end
def sluggable_source_column
@sluggable_source || :title
end
end
# Instantiemethoden
def to_param
slug
end
private
def should_generate_slug?
slug.blank? || send("#{self.class.sluggable_source_column}_changed?")
end
def generate_slug
source = send(self.class.sluggable_source_column)
return if source.blank?
base_slug = source.parameterize
self.slug = unique_slug(base_slug)
end
def unique_slug(base)
slug = base
counter = 1
while self.class.where(slug: slug).where.not(id: id).exists?
slug = "#{base}-#{counter}"
counter += 1
end
slug
end
end# app/models/article.rb
class Article < ApplicationRecord
include Sluggable
sluggable_source :title # Optioneel, :title als standaard
end
# app/models/product.rb
class Product < ApplicationRecord
include Sluggable
sluggable_source :name
end# app/controllers/concerns/pagination.rb
# Concern voor controllers
module Pagination
extend ActiveSupport::Concern
included do
helper_method :page_param, :per_page_param
end
private
def paginate(relation)
relation.page(page_param).per(per_page_param)
end
def page_param
params[:page]&.to_i || 1
end
def per_page_param
[params[:per_page]&.to_i || 25, 100].min
end
endConcerns zijn nuttig voor echt gedeelde code. Vermijd het maken van Concerns alleen om een Model "in te korten": dat verbergt complexiteit zonder die te verminderen.
Testen met RSpec
Vraag 10: Hoe RSpec-tests structureren in Rails?
RSpec is het standaard testframework voor Rails. Een goede teststructuur omvat Model specs, Controller specs, Service specs en integratietesten.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Factories met FactoryBot
let(:user) { build(:user) }
let(:admin) { build(:user, :admin) }
describe 'validations' do
it { is_expected.to validate_presence_of(:email) }
it { is_expected.to validate_uniqueness_of(:email).case_insensitive }
it 'valideert het e-mailformaat' do
user.email = 'invalid'
expect(user).not_to be_valid
expect(user.errors[:email]).to include('is invalid')
end
end
describe 'associations' do
it { is_expected.to have_many(:articles).dependent(:destroy) }
it { is_expected.to have_one(:profile) }
it { is_expected.to belong_to(:organization).optional }
end
describe '#full_name' do
it 'geeft voor- en achternaam gecombineerd terug' do
user = build(:user, first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq('John Doe')
end
it 'verwerkt ontbrekende achternaam' do
user = build(:user, first_name: 'John', last_name: nil)
expect(user.full_name).to eq('John')
end
end
describe '.active' do
it 'geeft alleen actieve gebruikers terug' do
active = create(:user, active: true)
inactive = create(:user, active: false)
expect(User.active).to include(active)
expect(User.active).not_to include(inactive)
end
end
end# spec/services/order_processor_spec.rb
require 'rails_helper'
RSpec.describe OrderProcessor do
let(:user) { create(:user) }
let(:product) { create(:product, stock_quantity: 10, price: 100) }
let(:order) { create(:order, user: user, items: [build(:order_item, product: product, quantity: 2)]) }
subject { described_class.new(order, payment_method: 'card') }
describe '#call' do
context 'wanneer de bestelling geldig is' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: true, transaction_id: 'txn_123')
)
end
it 'verwerkt de bestelling met succes' do
result = subject.call
expect(result).to be_success
expect(order.reload.status).to eq('completed')
end
it 'verlaagt de productvoorraad' do
expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
end
it 'verstuurt de bevestigingsmail' do
expect { subject.call }
.to have_enqueued_mail(OrderMailer, :confirmation)
.with(order)
end
end
context 'wanneer de betaling mislukt' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: false, error: 'Card declined')
)
end
it 'geeft een mislukkingsresultaat terug' do
result = subject.call
expect(result).to be_failure
expect(result.error).to include('Card declined')
end
it 'werkt de bestellingsstatus niet bij' do
expect { subject.call }.not_to change { order.reload.status }
end
end
end
end# spec/requests/api/v1/products_spec.rb
require 'rails_helper'
RSpec.describe 'API V1 Products', type: :request do
let(:user) { create(:user) }
let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } }
describe 'GET /api/v1/products' do
let!(:products) { create_list(:product, 3, :active) }
it 'geeft de productlijst terug' do
get '/api/v1/products', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'filtert op categorie' do
category = create(:category)
categorized = create(:product, category: category)
get '/api/v1/products', params: { category_id: category.id }, headers: headers
expect(json_response['data'].map { |p| p['id'] }).to eq([categorized.id])
end
end
describe 'POST /api/v1/products' do
let(:valid_params) do
{ product: { name: 'Nieuw Product', price: 99.99, category_id: create(:category).id } }
end
it 'maakt een nieuw product aan' do
expect {
post '/api/v1/products', params: valid_params, headers: headers
}.to change(Product, :count).by(1)
expect(response).to have_http_status(:created)
end
end
endBest practices: gebruik let voor data, describe voor methoden/contexten, context voor voorwaarden en it voor specifieke asserties. Eén test moet één ding testen.
Vraag 11: Hoe factories met FactoryBot gebruiken?
FactoryBot maakt het mogelijk om testdata declaratief en onderhoudbaar aan te maken. Factories vervangen statische fixtures.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
# Sequenties voor uniciteit
sequence(:email) { |n| "user#{n}@example.com" }
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
password { 'password123' }
confirmed_at { Time.current }
# Traits voor variaties
trait :admin do
role { 'admin' }
after(:create) do |user|
user.permissions.create!(name: 'admin_access')
end
end
trait :unconfirmed do
confirmed_at { nil }
end
trait :with_profile do
after(:create) do |user|
create(:profile, user: user)
end
end
trait :with_articles do
transient do
articles_count { 3 }
end
after(:create) do |user, evaluator|
create_list(:article, evaluator.articles_count, author: user)
end
end
# Geërfde factory
factory :admin_user do
admin
with_profile
end
end
end# spec/factories/orders.rb
FactoryBot.define do
factory :order do
user
status { 'pending' }
trait :with_items do
transient do
items_count { 2 }
end
after(:create) do |order, evaluator|
create_list(:order_item, evaluator.items_count, order: order)
order.recalculate_total!
end
end
trait :completed do
status { 'completed' }
processed_at { Time.current }
with_items
end
trait :high_value do
after(:create) do |order|
create(:order_item, order: order, quantity: 10, unit_price: 500)
order.recalculate_total!
end
end
end
end# Gebruik in tests
RSpec.describe OrderProcessor do
# build: niet-gepersisteerde instantie
let(:user) { build(:user) }
# create: gepersisteerd in DB
let(:order) { create(:order, :with_items, user: user) }
# create_list: meerdere instanties
let(:products) { create_list(:product, 5) }
# Traits combineren
let(:admin) { create(:user, :admin, :with_profile) }
# Attributen overschrijven
let(:expensive_order) { create(:order, :with_items, items_count: 10) }
# build_stubbed: sneller, voor unit tests
let(:stubbed_user) { build_stubbed(:user) }
endGeef de voorkeur aan build of build_stubbed boven create wanneer persistentie niet nodig is: dit versnelt de tests aanzienlijk.
Background Jobs
Vraag 12: Hoe Active Job en Sidekiq in Rails gebruiken?
Active Job biedt een uniforme interface voor background jobs, ongeacht het backend (Sidekiq, Resque, enz.). Sidekiq is de populaire keuze vanwege zijn performance met Redis.
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
# Retry-configuratie
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
discard_on ActiveJob::DeserializationError
# Sidekiq-opties (bij Sidekiq-backend)
sidekiq_options retry: 5, backtrace: true
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).call
rescue ActiveRecord::RecordNotFound
# Bestelling verwijderd tussen enqueue en uitvoering
Rails.logger.warn("Order #{order_id} not found, skipping job")
end
end# app/jobs/batch_email_job.rb
class BatchEmailJob < ApplicationJob
queue_as :mailers
# Rate limiting met Sidekiq Enterprise of throttle-gem
sidekiq_options throttle: { threshold: 100, period: 1.minute }
def perform(user_ids, template_id)
template = EmailTemplate.find(template_id)
User.where(id: user_ids).find_each do |user|
UserMailer.custom_email(user, template).deliver_later
end
end
end# Jobs in de wachtrij plaatsen
# Onmiddellijk
ProcessOrderJob.perform_later(order.id)
# Vertraagd
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)
# Op een specifiek tijdstip
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
# Specifieke wachtrij
ProcessOrderJob.set(queue: :critical).perform_later(order.id)
# Synchroon (voor tests of debugging)
ProcessOrderJob.perform_now(order.id)# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3] # Hoge prioriteit, gewicht 3
- [default, 2] # Gemiddelde prioriteit, gewicht 2
- [mailers, 1] # Lage prioriteit, gewicht 1
- [low, 1]
:schedule:
cleanup_job:
cron: '0 3 * * *' # Elke dag om 3 uur
class: CleanupJobActive Job abstraheert het backend, maar toegang tot specifieke functies (batches, rate limiting) vereist vaak koppeling met het gekozen backend.
Klaar om je Ruby on Rails gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
API-ontwikkeling
Vraag 13: Hoe een RESTful API met Rails bouwen?
Rails maakt het eenvoudig om JSON-API's te bouwen met API-only Controllers en serializers. Een goede API is geversioneerd, gedocumenteerd en veilig.
# app/controllers/api/v1/base_controller.rb
module Api
module V1
class BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_token!
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def authenticate_token!
authenticate_or_request_with_http_token do |token, options|
@current_user = User.find_by(api_token: token)
end
end
def current_user
@current_user
end
def not_found(exception)
render json: { error: 'Resource niet gevonden', details: exception.message },
status: :not_found
end
def unprocessable_entity(exception)
render json: { error: 'Validatie mislukt', details: exception.record.errors },
status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: 'Ongeldig verzoek', details: exception.message },
status: :bad_request
end
end
end
end# app/controllers/api/v1/products_controller.rb
module Api
module V1
class ProductsController < BaseController
before_action :set_product, only: [:show, :update, :destroy]
def index
@products = Product.active
.includes(:category)
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: ProductSerializer.new(@products).serializable_hash,
meta: pagination_meta(@products)
}
end
def show
render json: ProductSerializer.new(@product, include: [:category, :reviews])
end
def create
@product = Product.new(product_params)
@product.save!
render json: ProductSerializer.new(@product), status: :created
end
def update
@product.update!(product_params)
render json: ProductSerializer.new(@product)
end
def destroy
@product.destroy!
head :no_content
end
private
def set_product
@product = Product.find(params[:id])
end
def product_params
params.require(:product).permit(:name, :description, :price, :category_id)
end
def pagination_meta(collection)
{
current_page: collection.current_page,
total_pages: collection.total_pages,
total_count: collection.total_count
}
end
end
end
end# app/serializers/product_serializer.rb
# Met de jsonapi-serializer-gem
class ProductSerializer
include JSONAPI::Serializer
attributes :id, :name, :description, :price, :created_at
attribute :formatted_price do |product|
"$#{product.price.to_f.round(2)}"
end
belongs_to :category
has_many :reviews
link :self do |product|
Rails.application.routes.url_helpers.api_v1_product_url(product)
end
endAPI-best practices: versioneren via namespace, geschikte HTTP-codes gebruiken, collecties pagineren en duidelijke foutmeldingen geven.
Vraag 14: Hoe JWT-authenticatie in Rails implementeren?
JWT (JSON Web Tokens) is een populaire stateless authenticatiemethode voor API's. De token codeert de identiteit en geldigheid van de gebruiker.
# app/services/jwt_service.rb
class JwtService
SECRET_KEY = Rails.application.credentials.secret_key_base
ALGORITHM = 'HS256'.freeze
class << self
def encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
payload[:iat] = Time.current.to_i
JWT.encode(payload, SECRET_KEY, ALGORITHM)
end
def decode(token)
decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM)
HashWithIndifferentAccess.new(decoded.first)
rescue JWT::ExpiredSignature
raise AuthenticationError, 'Token verlopen'
rescue JWT::DecodeError
raise AuthenticationError, 'Ongeldig token'
end
end
end# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ActionController::API
def login
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
token = JwtService.encode(user_id: user.id)
render json: {
token: token,
user: UserSerializer.new(user),
expires_at: 24.hours.from_now
}
else
render json: { error: 'Ongeldige inloggegevens' }, status: :unauthorized
end
end
def refresh
token = JwtService.encode(user_id: current_user.id)
render json: { token: token, expires_at: 24.hours.from_now }
end
end
end
end# app/controllers/concerns/jwt_authenticatable.rb
module JwtAuthenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_jwt!
end
private
def authenticate_jwt!
header = request.headers['Authorization']
token = header&.split(' ')&.last
raise AuthenticationError, 'Token ontbreekt' unless token
decoded = JwtService.decode(token)
@current_user = User.find(decoded[:user_id])
rescue AuthenticationError => e
render json: { error: e.message }, status: :unauthorized
rescue ActiveRecord::RecordNotFound
render json: { error: 'Gebruiker niet gevonden' }, status: :unauthorized
end
def current_user
@current_user
end
endVoor productie overwegen: refresh tokens, blacklisting van tokens bij logout en korte verlooptijden. Gems zoals devise-jwt vereenvoudigen de implementatie.
Caching en performance
Vraag 15: Hoe caching in Rails implementeren?
Rails biedt verschillende cachingniveaus: fragment caching, Russian Doll caching, low-level caching. De keuze hangt af van het gebruiksgeval.
# config/environments/production.rb
config.action_controller.perform_caching = true
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
namespace: 'myapp:cache',
expires_in: 1.day,
race_condition_ttl: 10.seconds
}<%# app/views/products/index.html.erb %>
<%# Fragment caching met automatische cache-sleutel %>
<% @products.each do |product| %>
<%# Cache gebaseerd op updated_at van product %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<%# Russian Doll caching - geneste cache %>
<% cache ['v1', @category] do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache ['v1', product] do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
<%# Voorwaardelijke cache %>
<% cache_if current_user.nil?, @product do %>
<%= render @product %>
<% end %># app/models/product.rb
class Product < ApplicationRecord
# Touch op de parent om Russian Doll-cache te invalideren
belongs_to :category, touch: true
# Aangepaste cache-sleutel
def cache_key_with_version
"#{super}/#{reviews.maximum(:updated_at)&.to_i}"
end
end# Low-level caching in services
class DashboardStatsService
def call
Rails.cache.fetch('dashboard:stats', expires_in: 15.minutes) do
{
total_users: User.count,
active_users: User.where('last_sign_in_at > ?', 30.days.ago).count,
total_orders: Order.completed.count,
revenue_mtd: Order.completed.where(created_at: Time.current.beginning_of_month..).sum(:total)
}
end
end
end
# Cache met bescherming tegen race conditions
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
Product.bestsellers.limit(10).to_a
end
# Expliciete invalidatie
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')Russian Doll caching is effectief omdat alleen gewijzigde fragmenten opnieuw worden gegenereerd. Gebruik touch: true op associaties om invalidatie te verspreiden.
Vraag 16: Hoe de performance van een Rails-applicatie optimaliseren?
Rails-optimalisatie omvat meerdere aspecten: DB-queries, caching, assets en architectuur. Een methodische aanpak met monitoring is essentieel.
# Database-optimalisatie
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
prepared_statements: true
advisory_locks: true
# app/models/order.rb
class Order < ApplicationRecord
# Samengestelde indexen voor frequente queries
# add_index :orders, [:user_id, :status, :created_at]
# Alleen de benodigde kolommen selecteren
scope :summary, -> { select(:id, :status, :total, :created_at) }
# Batchverwerking voor grote volumes
def self.process_pending
pending.find_each(batch_size: 1000) do |order|
ProcessOrderJob.perform_later(order.id)
end
end
# Herhalende berekeningen vermijden
def self.revenue_by_month
completed
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
end
end# Geheugenoptimalisatie
# config/puma.rb
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count
preload_app!
before_fork do
ActiveRecord::Base.connection_pool.disconnect!
end
on_worker_boot do
ActiveRecord::Base.establish_connection
end# Profiling met rack-mini-profiler
# Gemfile
group :development do
gem 'rack-mini-profiler'
gem 'memory_profiler'
gem 'stackprof'
end
# config/initializers/mini_profiler.rb
if defined?(Rack::MiniProfiler)
Rack::MiniProfiler.config.position = 'bottom-right'
Rack::MiniProfiler.config.start_hidden = true
end# Lazy loading en paginering
class ProductsController < ApplicationController
def index
@products = Product.active
.includes(:category, :primary_image)
.page(params[:page])
.per(24)
# Prefetch voor de volgende pagina
if @products.next_page
Rails.cache.fetch("products:page:#{@products.next_page}", expires_in: 5.minutes) do
Product.active.page(@products.next_page).per(24).to_a
end
end
end
endEssentiële tools: rack-mini-profiler voor profiling, bullet voor N+1-detectie, New Relic of Scout voor productiemonitoring.
Beveiliging
Vraag 17: Wat zijn de beveiligingsbest practices in Rails?
Rails bevat standaardbeveiligingen tegen veelvoorkomende kwetsbaarheden. Het is cruciaal deze beveiligingen te begrijpen en correct te configureren.
# CSRF-bescherming
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Standaard ingeschakeld, gooit een exception als het token ongeldig is
protect_from_forgery with: :exception
# Voor API's :null_session gebruiken
# protect_from_forgery with: :null_session
end
# In views wordt het token automatisch in formulieren opgenomen
# <%= form_with ... %> bevat authenticity_token
# Voor AJAX-verzoeken
# Voeg de header X-CSRF-Token toe met de waarde van csrf_meta_tags# SQL Injection-preventie
# ✅ Geïnterpoleerde parameters worden automatisch ge-escaped
User.where('email = ?', params[:email])
User.where(email: params[:email])
# ❌ GEVAAR - Directe interpolatie
User.where("email = '#{params[:email]}'")
# ✅ Voor dynamische ORDER-clausules
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)# XSS-bescherming
# Rails escapet HTML in views automatisch
# ✅ Automatisch ge-escaped
<%= user.name %>
# ❌ Gevaarlijk - niet ge-escapete inhoud
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
# ✅ Voor veilige HTML, sanitize gebruiken
<%= sanitize user.bio, tags: %w[p br strong em] %># Strong Parameters
class UsersController < ApplicationController
def update
@user.update!(user_params)
end
private
def user_params
# Expliciete whitelist van toegestane attributen
params.require(:user).permit(:name, :email, :avatar)
# Alleen voor administrators
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :active)
else
params.require(:user).permit(:name, :email)
end
end
end# Beveiligingsheaders
# config/initializers/secure_headers.rb
Rails.application.config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
'X-XSS-Protection' => '1; mode=block',
'X-Content-Type-Options' => 'nosniff',
'X-Download-Options' => 'noopen',
'X-Permitted-Cross-Domain-Policies' => 'none',
'Referrer-Policy' => 'strict-origin-when-cross-origin'
}
# Content Security Policy
Rails.application.config.content_security_policy do |policy|
policy.default_src :self
policy.script_src :self
policy.style_src :self, :unsafe_inline
policy.img_src :self, :data, 'https:'
endRegelmatig auditen met brakeman (statische beveiligingsanalyse) en gems up-to-date houden met bundle audit.
Vraag 18: Hoe authenticatie en autorisatie in Rails afhandelen?
Authenticatie verifieert de identiteit, autorisatie regelt de rechten. Devise beheert auth, Pundit of CanCanCan beheren autorisatie.
# Devise-setup
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:confirmable, :lockable, :trackable
enum role: { user: 0, moderator: 1, admin: 2 }
def admin?
role == 'admin'
end
end# Pundit-policies
# app/policies/article_policy.rb
class ArticlePolicy < ApplicationPolicy
def index?
true
end
def show?
record.published? || owner_or_admin?
end
def create?
user.present?
end
def update?
owner_or_admin?
end
def destroy?
owner_or_admin?
end
def publish?
user&.admin? || user&.moderator?
end
# Scope voor collecties
class Scope < Scope
def resolve
if user&.admin?
scope.all
elsif user
scope.where(published: true).or(scope.where(author: user))
else
scope.where(published: true)
end
end
end
private
def owner_or_admin?
user&.admin? || record.author == user
end
end# Controller met Pundit
class ArticlesController < ApplicationController
include Pundit::Authorization
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
def index
@articles = policy_scope(Article).includes(:author).page(params[:page])
end
def show
@article = Article.find(params[:id])
authorize @article
end
def update
@article = Article.find(params[:id])
authorize @article
if @article.update(article_params)
redirect_to @article, notice: 'Artikel bijgewerkt.'
else
render :edit, status: :unprocessable_entity
end
end
def publish
@article = Article.find(params[:id])
authorize @article
@article.update!(published: true, published_at: Time.current)
redirect_to @article, notice: 'Artikel gepubliceerd.'
end
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "U bent niet bevoegd om deze actie uit te voeren."
redirect_back(fallback_location: root_path)
end
endPundit is explicieter en beter testbaar dan CanCanCan. Elke actie heeft een corresponderende policy-methode en scopes filteren collecties automatisch.
Geavanceerd Rails
Vraag 19: Leg het Repository-patroon in Rails uit
Het Repository-patroon isoleert de data access-logica van de rest van de applicatie. Hoewel Rails Active Record gebruikt (een ander patroon), kan Repository nuttig zijn voor complexe gevallen.
# app/repositories/base_repository.rb
class BaseRepository
def initialize(model_class)
@model_class = model_class
end
def all
@model_class.all
end
def find(id)
@model_class.find(id)
end
def find_by(attributes)
@model_class.find_by(attributes)
end
def create(attributes)
@model_class.create(attributes)
end
def update(record, attributes)
record.update(attributes)
end
def delete(record)
record.destroy
end
end# app/repositories/product_repository.rb
class ProductRepository < BaseRepository
def initialize
super(Product)
end
def active
@model_class.where(active: true)
end
def in_category(category_id)
@model_class.where(category_id: category_id)
end
def search(query)
@model_class.where('name ILIKE ? OR description ILIKE ?',
"%#{query}%", "%#{query}%")
end
def with_stock
@model_class.where('stock_quantity > 0')
end
def bestsellers(limit: 10)
@model_class
.joins(:order_items)
.group(:id)
.order('COUNT(order_items.id) DESC')
.limit(limit)
end
def for_homepage
active
.with_stock
.includes(:category, :primary_image)
.order(featured: :desc, created_at: :desc)
.limit(12)
end
end# Gebruik in een service
class ProductSearchService
def initialize(repository: ProductRepository.new)
@repository = repository
end
def call(params)
products = @repository.active
products = products.in_category(params[:category]) if params[:category]
products = products.search(params[:query]) if params[:query].present?
products = products.with_stock if params[:in_stock]
products
end
end
# Maakt testen met mocks gemakkelijk
RSpec.describe ProductSearchService do
let(:repository) { instance_double(ProductRepository) }
let(:service) { described_class.new(repository: repository) }
it 'filtert op categorie' do
products = double('products')
allow(repository).to receive(:active).and_return(products)
allow(products).to receive(:in_category).with(1).and_return(products)
service.call(category: 1)
expect(products).to have_received(:in_category).with(1)
end
endRepository is optioneel in Rails omdat Active Record al een uitstekend patroon is. Gebruik het voor complexe queries of wanneer storage-isolatie belangrijk is.
Vraag 20: Hoe het CQRS-patroon in Rails implementeren?
CQRS (Command Query Responsibility Segregation) scheidt lees- en schrijfoperaties. In Rails vertaalt dit zich naar afzonderlijke klassen voor queries en commands.
# app/commands/base_command.rb
class BaseCommand
include ActiveModel::Validations
def self.call(*args)
new(*args).call
end
def call
return failure(errors) unless valid?
execute
end
private
def execute
raise NotImplementedError
end
def success(data = nil)
CommandResult.success(data)
end
def failure(errors)
CommandResult.failure(errors)
end
end
CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do
def success? = success
def failure? = !success
def self.success(data)
new(success: true, data: data, errors: [])
end
def self.failure(errors)
new(success: false, data: nil, errors: Array(errors))
end
end# app/commands/orders/create_order_command.rb
module Orders
class CreateOrderCommand < BaseCommand
attr_reader :user, :items, :shipping_address
validates :user, presence: true
validates :items, presence: true
validate :validate_items_availability
def initialize(user:, items:, shipping_address:)
@user = user
@items = items
@shipping_address = shipping_address
end
private
def execute
order = nil
ActiveRecord::Base.transaction do
order = Order.create!(
user: user,
shipping_address: shipping_address,
status: 'pending'
)
items.each do |item|
order.items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
unit_price: Product.find(item[:product_id]).price
)
end
order.calculate_total!
end
OrderCreatedEvent.broadcast(order)
success(order)
rescue ActiveRecord::RecordInvalid => e
failure(e.message)
end
def validate_items_availability
items.each do |item|
product = Product.find_by(id: item[:product_id])
unless product&.stock_quantity&.>= item[:quantity]
errors.add(:items, "Product #{item[:product_id]} niet beschikbaar")
end
end
end
end
end# app/queries/orders/user_orders_query.rb
module Orders
class UserOrdersQuery
def initialize(user, params = {})
@user = user
@params = params
end
def call
orders = @user.orders.includes(:items, items: :product)
orders = apply_status_filter(orders)
orders = apply_date_filter(orders)
orders = apply_sorting(orders)
orders.page(@params[:page]).per(@params[:per_page] || 20)
end
private
def apply_status_filter(orders)
return orders unless @params[:status]
orders.where(status: @params[:status])
end
def apply_date_filter(orders)
orders = orders.where('created_at >= ?', @params[:from]) if @params[:from]
orders = orders.where('created_at <= ?', @params[:to]) if @params[:to]
orders
end
def apply_sorting(orders)
case @params[:sort]
when 'oldest' then orders.order(created_at: :asc)
when 'total_desc' then orders.order(total: :desc)
else orders.order(created_at: :desc)
end
end
end
end# Controller die CQRS gebruikt
class OrdersController < ApplicationController
def index
@orders = Orders::UserOrdersQuery.new(current_user, filter_params).call
end
def create
result = Orders::CreateOrderCommand.call(
user: current_user,
items: order_params[:items],
shipping_address: order_params[:shipping_address]
)
if result.success?
redirect_to result.data, notice: 'Bestelling aangemaakt!'
else
flash.now[:alert] = result.errors.join(', ')
render :new, status: :unprocessable_entity
end
end
endCQRS schittert in complexe applicaties met asymmetrische lees-/schrijfbehoeften. Voor eenvoudig CRUD is het over-engineering.
Vraag 21: Hoe WebSockets met Action Cable afhandelen?
Action Cable integreert WebSockets in Rails voor bidirectionele realtime communicatie. Het gebruikt Redis voor synchronisatie tussen servers.
# app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
# Via sessie-cookie
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
# Via JWT voor API's
elsif verified_user = verify_jwt_token
verified_user
else
reject_unauthorized_connection
end
end
def verify_jwt_token
token = request.params[:token]
return nil unless token
decoded = JwtService.decode(token)
User.find(decoded[:user_id])
rescue
nil
end
end
end# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = ChatRoom.find(params[:room_id])
# Rechten controleren
unless @room.accessible_by?(current_user)
reject
return
end
stream_for @room
# Anderen op de hoogte stellen van aanwezigheid
broadcast_presence(:joined)
end
def unsubscribed
broadcast_presence(:left) if @room
end
def send_message(data)
message = @room.messages.create!(
user: current_user,
content: data['content']
)
# Naar alle abonnees broadcasten
ChatChannel.broadcast_to(@room, {
type: 'message',
message: MessageSerializer.new(message).as_json
})
end
def typing
ChatChannel.broadcast_to(@room, {
type: 'typing',
user: current_user.name
})
end
private
def broadcast_presence(action)
ChatChannel.broadcast_to(@room, {
type: 'presence',
action: action,
user: current_user.name,
online_count: @room.online_users_count
})
end
endimport consumer from "./consumer"
const chatChannel = consumer.subscriptions.create(
{ channel: "ChatChannel", room_id: roomId },
{
connected() {
console.log("Connected to chat")
},
disconnected() {
console.log("Disconnected from chat")
},
received(data) {
switch(data.type) {
case 'message':
this.appendMessage(data.message)
break
case 'typing':
this.showTypingIndicator(data.user)
break
case 'presence':
this.updatePresence(data)
break
}
},
sendMessage(content) {
this.perform('send_message', { content: content })
},
notifyTyping() {
this.perform('typing')
}
}
)Action Cable handelt heraansluitingen en synchronisatie automatisch af. In productie Redis als adapter configureren en schalen op basis van gelijktijdige verbindingen.
Vraag 22: Hoe multi-tenancy in Rails implementeren?
Multi-tenancy stelt een applicatie in staat om meerdere geïsoleerde klanten (tenants) te bedienen. Drie hoofdaanpakken: op database-, schema- of rij-niveau.
# Multitenancy op rij-niveau met ActsAsTenant of handmatig
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
# Standaardscope op de huidige tenant
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
# Tenant-validatie
before_validation :set_tenant, on: :create
end
private
def set_tenant
self.tenant ||= Current.tenant
end
end
# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
attribute :tenant, :user
end# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
before_action :set_current_tenant
private
def set_current_tenant
Current.tenant = resolve_tenant
Current.user = current_user
end
def resolve_tenant
# Via subdomein
if request.subdomain.present? && request.subdomain != 'www'
Tenant.find_by!(subdomain: request.subdomain)
# Via header (voor API's)
elsif request.headers['X-Tenant-ID'].present?
Tenant.find(request.headers['X-Tenant-ID'])
# Via gebruiker
elsif current_user
current_user.tenant
end
rescue ActiveRecord::RecordNotFound
redirect_to root_url(subdomain: 'www'), alert: 'Tenant niet gevonden'
end
end# app/models/project.rb
class Project < ApplicationRecord
include TenantScoped
has_many :tasks
belongs_to :owner, class_name: 'User'
end
# app/models/user.rb
class User < ApplicationRecord
include TenantScoped
has_many :projects, foreign_key: :owner_id
# Administrators kunnen tot meerdere tenants behoren
has_many :tenant_memberships
has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end# Op schema-niveau met de Apartment-gem (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:subdomain) }
end
# Gebruik
Apartment::Tenant.switch('acme') do
# Alle queries in dit blok gebruiken het 'acme'-schema
Project.all # SELECT * FROM acme.projects
endRij-niveau is het eenvoudigst maar vereist constante aandacht voor lekken. Schema-niveau biedt betere isolatie maar bemoeilijkt migraties. Kies op basis van beveiligings- en schaalbaarheidseisen.
Vraag 23: Hoe een microservices-architectuur met Rails opzetten?
Rails kan dienen als basis voor een microservices-architectuur met communicatie via HTTP/gRPC of message queues. De sleutel is het goed definiëren van de grenzen.
# HTTP-service-client
# app/services/payment_service_client.rb
class PaymentServiceClient
include HTTParty
base_uri ENV.fetch('PAYMENT_SERVICE_URL')
def initialize
@options = {
headers: {
'Content-Type' => 'application/json',
'X-Service-Token' => ENV.fetch('SERVICE_TOKEN')
},
timeout: 10
}
end
def create_charge(amount:, currency:, source:, metadata: {})
response = self.class.post('/charges', @options.merge(
body: { amount: amount, currency: currency, source: source, metadata: metadata }.to_json
))
handle_response(response)
end
def get_charge(charge_id)
response = self.class.get("/charges/#{charge_id}", @options)
handle_response(response)
end
private
def handle_response(response)
case response.code
when 200..299
ServiceResult.success(response.parsed_response)
when 400..499
ServiceResult.failure(response.parsed_response['error'], code: response.code)
else
ServiceResult.failure('Service niet beschikbaar', code: response.code)
end
rescue Net::OpenTimeout, Net::ReadTimeout
ServiceResult.failure('Service-timeout')
end
end# Event-driven communicatie met Sidekiq/Redis
# app/events/order_events.rb
module OrderEvents
class Created
include Wisper::Publisher
def call(order)
broadcast(:order_created, order)
end
end
end
# app/listeners/inventory_listener.rb
class InventoryListener
def order_created(order)
order.items.each do |item|
InventoryServiceClient.new.reserve_stock(
product_id: item.product_id,
quantity: item.quantity,
reference: order.id
)
end
end
end
# config/initializers/wisper.rb
Wisper.subscribe(InventoryListener.new, async: true)
Wisper.subscribe(NotificationListener.new, async: true)# API Gateway-patroon
# app/controllers/api/v1/gateway_controller.rb
module Api
module V1
class GatewayController < BaseController
# Meerdere services aggregeren
def dashboard
results = Parallel.map([:orders, :inventory, :analytics], in_threads: 3) do |service|
fetch_from_service(service)
end
render json: {
orders: results[0],
inventory: results[1],
analytics: results[2]
}
end
private
def fetch_from_service(service)
case service
when :orders
OrderServiceClient.new.recent_orders(limit: 5)
when :inventory
InventoryServiceClient.new.low_stock_alerts
when :analytics
AnalyticsServiceClient.new.daily_summary
end
rescue => e
{ error: "#{service} niet beschikbaar", message: e.message }
end
end
end
endVoor Rails-microservices: heldere API-contracten definiëren (OpenAPI), circuit breakers implementeren (gem circuitbox) en distributed tracing gebruiken (gem opentelemetry).
Vraag 24: Hoe een Rails-applicatie naar productie deployen?
Moderne Rails-deployment maakt gebruik van containers of PaaS. Een robuuste productieconfiguratie omvat assets, database en monitoring.
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
# Assets
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.assets.compile = false
config.assets.digest = true
# Logging
config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym
config.log_tags = [:request_id]
config.logger = ActiveSupport::Logger.new(STDOUT)
.tap { |logger| logger.formatter = Logger::Formatter.new }
.then { |logger| ActiveSupport::TaggedLogging.new(logger) }
# Cache
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
expires_in: 1.day
}
# SSL forceren
config.force_ssl = true
config.ssl_options = { hsts: { subdomains: true } }
# Action Mailer
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV['SMTP_HOST'],
port: ENV['SMTP_PORT'],
user_name: ENV['SMTP_USER'],
password: ENV['SMTP_PASSWORD'],
authentication: :plain,
enable_starttls_auto: true
}
end# Dockerfile
FROM ruby:3.3-alpine AS builder
RUN apk add --no-cache build-base postgresql-dev nodejs yarn
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment true && \
bundle config set --local without 'development test' && \
bundle install
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN bundle exec rails assets:precompile
# Productie-image
FROM ruby:3.3-alpine
RUN apk add --no-cache postgresql-client tzdata
WORKDIR /app
COPY /app /app
COPY /usr/local/bundle /usr/local/bundle
ENV RAILS_ENV=production
ENV RAILS_LOG_TO_STDOUT=true
EXPOSE 3000
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]# docker-compose.production.yml
version: '3.8'
services:
web:
build: .
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
- SECRET_KEY_BASE=${SECRET_KEY_BASE}
depends_on:
- db
- redis
deploy:
replicas: 3
resources:
limits:
memory: 512M
sidekiq:
build: .
command: bundle exec sidekiq
environment:
- DATABASE_URL=postgres://user:pass@db/app_production
- REDIS_URL=redis://redis:6379/0
depends_on:
- db
- redis
deploy:
replicas: 2
db:
image: postgres:15-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${DB_PASSWORD}
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:Productie-checklist: SSL verplicht, secrets via ENV, health checks, geautomatiseerde DB-backups, monitoring (APM + logs + metrics) en geconfigureerde alerting.
Vraag 25: Wat zijn de nieuwe features in Rails 7+ die je moet kennen?
Rails 7+ brengt belangrijke wijzigingen: Hotwire standaard, import maps, verbeterde versleutelde credentials en talloze optimalisaties.
# Hotwire - Turbo Frames
# app/views/articles/index.html.erb
<%= turbo_frame_tag "articles" do %>
<% @articles.each do |article| %>
<%= turbo_frame_tag dom_id(article) do %>
<%= render article %>
<% end %>
<% end %>
<%= link_to "Meer laden", articles_path(page: @page + 1),
data: { turbo_frame: "articles" } %>
<% end %>
# Turbo Streams voor realtime updates
# app/controllers/comments_controller.rb
def create
@comment = @article.comments.create!(comment_params.merge(user: current_user))
respond_to do |format|
format.turbo_stream
format.html { redirect_to @article }
end
end
# app/views/comments/create.turbo_stream.erb
<%= turbo_stream.append "comments", @comment %>
<%= turbo_stream.update "comments_count", @article.comments.count %>
<%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %># Stimulus-controllers
# app/javascript/controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
import { debounce } from "lodash-es"
export default class extends Controller {
static targets = ["input", "results"]
static values = { url: String }
connect() {
this.search = debounce(this.search.bind(this), 300)
}
async search() {
const query = this.inputTarget.value
if (query.length < 2) return
const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`)
this.resultsTarget.innerHTML = await response.text()
}
}# Import Maps (zonder JavaScript-bundler)
# config/importmap.rb
pin "application"
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers"
# Pins vanaf CDN
pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js"# Active Record Encryption (Rails 7+)
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true # Zoekopdrachten mogelijk
encrypts :phone_number # Niet-deterministisch standaard
encrypts :ssn, deterministic: true, downcase: true
end
# config/credentials.yml.enc
active_record_encryption:
primary_key: abc123...
deterministic_key: def456...
key_derivation_salt: ghi789...# Verbeteringen in de query-interface
# Rails 7.1+
# Asynchrone queries
users = User.where(active: true).load_async
# Verder verwerken terwijl de query loopt
# Resultaten openen met users.to_a
# Common Table Expressions (CTE)
User.with(
recent_orders: Order.where('created_at > ?', 30.days.ago)
).joins('JOIN recent_orders ON recent_orders.user_id = users.id')
# Automatische inverse_of-detectie
class Author < ApplicationRecord
has_many :books # inverse_of automatisch gedetecteerd
end
# Strict loading standaard (vermijdt N+1)
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
endRails 7+ geeft de voorkeur aan eenvoud (geen Webpack standaard) en HTML-over-the-wire met Hotwire. Deze aanpak vermindert de JavaScript-complexiteit en biedt tegelijkertijd een moderne gebruikerservaring.
Conclusie
Ruby on Rails-sollicitatiegesprekken beoordelen de beheersing van het hele framework en het begrip van de conventies. Belangrijke punten om te onthouden:
✅ Grondbeginselen: MVC, Active Record, migraties, validaties en associaties
✅ Architectuur: Service Objects, Concerns, Query Objects en CQRS-patronen
✅ Performance: N+1-queries, caching (fragment, Russian Doll, low-level), eager loading
✅ Testen: RSpec, FactoryBot, request specs en testing best practices
✅ Beveiliging: CSRF, SQL injection, XSS, Strong Parameters en authenticatie/autorisatie
✅ API's: RESTful design, JWT, serializers en versionering
✅ Productie: background jobs, WebSockets, deployment en monitoring
De Rails-filosofie (Convention over Configuration, DRY en Rails Way) stuurt alle architecturale beslissingen. Deze principes beheersen en weten wanneer ervan af te wijken, toont solide expertise.
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Tags
Delen
Gerelateerde artikelen

ActiveRecord: N+1-queryproblemen oplossen in Ruby on Rails
Volledige gids voor het detecteren en oplossen van N+1-queries in Rails met ActiveRecord. Beheers includes, preload, eager_load en geautomatiseerde detectietools.

Ruby on Rails 7: Hotwire en Turbo voor Reactieve Applicaties
Volledige gids over Hotwire en Turbo in Rails 7. Bouw reactieve applicaties zonder JavaScript met Turbo Drive, Frames en Streams.

Rails API-modus in 2026: RESTful API's bouwen, JSON-serialisatie en sollicitatievragen
Praktische handleiding voor Rails 8.1 API-modus: RESTful routeontwerp, serialisatie met Alba en jsonapi-serializer, JWT-authenticatie, gecentraliseerde foutafhandeling, paginering en veelgestelde sollicitatievragen voor Ruby on Rails API-ontwikkelaars.