REST API mit Ruby on Rails: Representer und JSON:API
Ein Controller mit render json: @article funktioniert am ersten Tag. Nach ein paar Wochen hängt die mobile App an jedem Spaltennamen deiner Tabelle, und die neue Spalte internal_notes steht plötzlich in jeder Antwort. Wer als Entwickler eine REST API mit Ruby on Rails erstellen will, braucht deshalb eine Schicht zwischen Model und Ausgabe. Hier siehst du, wie Representer und Serializer diese Aufgabe übernehmen, welche Gems 2026 noch gepflegt werden und was zu Statuscodes, Sicherheit, Paginierung und Versionierung gehört.
REST API in Ruby on Rails: mehr als CRUD
REST steht für Representational State Transfer. Roy Fielding hat den Architekturstil im Jahr 2000 in seiner Dissertation beschrieben. Anders als SOAP (Simple Object Access Protocol) ist REST kein eigenes Protokoll. Der Begriff bezeichnet Regeln für die Interaktion zwischen Client und Server über ein vorhandenes Protokoll, in der Regel HTTP. Der Kern: Ein Client arbeitet mit Ressourcen, die über einen Uniform Resource Identifier (URI) erreichbar sind, und bekommt vom Server eine Repräsentation davon, also eine Darstellung als JSON, XML oder HTML. Die Ressource „Artikel 42“ ist etwas anderes als das Dokument, das du für sie ausgibst.
Dazu kommen ein paar Einschränkungen, die häufig untergehen. Die Kommunikation ist zustandslos: Der Server speichert keine Sitzung, jede Anfrage enthält alle erforderlichen Daten, etwa das Token für den Zugriff. Weil jeder Aufruf zustandslos bleibt, kannst du beliebig viele Server hinter einen Load Balancer stellen. Auch ein mehrschichtiges System mit Proxy und Cache dazwischen ist ausdrücklich vorgesehen. Client und Server bleiben unabhängig voneinander, solange die einheitliche Schnittstelle stabil ist. Deshalb funktionieren RESTful APIs im Internet mit jedem System, das HTTP spricht. Jede Methode hat eine feste Bedeutung, die folgenden fünf wirst du ständig verwenden:
- GET liest eine Ressource oder Liste und ändert nichts.
- POST legt eine Ressource an, der Server vergibt die ID.
- PUT ersetzt die Ressource vollständig, der Client schickt die ganze Repräsentation.
- PATCH ändert nur die mitgeschickten Felder.
- DELETE entfernt die Ressource.
Zwischen PUT und PATCH liegt ein handfester Unterschied. PUT und DELETE sind laut RFC 9110 (HTTP Semantics) idempotent: Zwei identische Anfragen hinterlassen denselben Zustand wie eine. Ein Client darf sie nach einem Timeout also gefahrlos wiederholen. PATCH garantiert das nicht, POST ebenfalls nicht. Rails leitet mit resources :articles sowohl PATCH als auch PUT auf die Action update. Wer echte PUT-Semantik will, muss fehlende Felder im Request selbst auf ihren Standardwert setzen.
Bei den Statuscodes reichen in der Praxis wenige:
| Situation | Code | Rails-Symbol |
|---|---|---|
| Lesen oder Ändern erfolgreich | 200 | :ok |
Ressource angelegt (mit Location-Header) |
201 | :created |
| Gelöscht, kein Inhalt im Body | 204 | :no_content |
| Nicht angemeldet / angemeldet, aber ohne Recht | 401 / 403 | :unauthorized / :forbidden |
| Ressource existiert nicht | 404 | :not_found |
| Validierung fehlgeschlagen | 422 | :unprocessable_content |
Der Code 422 heißt seit RFC 9110 „Unprocessable Content“. Aktuelle Rails-Scaffolds schreiben deshalb :unprocessable_content, das ältere :unprocessable_entity erzeugt mit Rack 3.1 eine Warnung.
Representer und Serializer: die Darstellung gehört nicht ins Model
as_json im Model zu überschreiben, klappt bei Anwendungen mit einem einzigen Client. Spätestens wenn die Admin-Oberfläche andere Felder sehen soll als die öffentliche App, wird das Model zur Ablage für Darstellungslogik. Ein Serializer ist eine eigene Klasse, die festlegt, welche Attribute ein Objekt nach außen zeigt. Ein Beispiel: Die mobile App sieht nur die bestimmten Felder, die sie braucht, die interne Anwendung zusätzlich Notizen und Status. Ein Representer geht einen Schritt weiter: Er kennt das Format in beide Richtungen, er rendert also JSON und parst eingehendes JSON zurück ins Objekt.
Das Gem Representable aus dem Trailblazer-Umfeld folgt genau diesem Muster. Für die Implementierung beschreibt ein Decorator die Felder einmal:
require "representable/json"
class ArticleRepresenter < Representable::Decorator
include Representable::JSON
property :id, writeable: false
property :title
property :body
property :published_at, writeable: false
end
# Rendern
ArticleRepresenter.new(article).to_json
# Parsen: setzt title und body über die Setter des Objekts
article = Article.new
ArticleRepresenter.new(article).from_json(request.raw_post)
article.save
Der Vorteil: Eingabe und Ausgabe können nicht auseinanderlaufen, weil beide aus derselben Definition entstehen. writeable: false sorgt dafür, dass ein Client die ID nicht von außen überschreibt. Validierung bleibt trotzdem Aufgabe des Models oder eines Form-Objekts.
Jbuilder, Alba, Representable und jsonapi-serializer im Vergleich
Die Tabelle zeigt die Werkzeuge, die Entwickler für Rails-APIs am häufigsten verwenden. Den Stand habe ich auf RubyGems geprüft (letzte stabile Version, Stand November 2026).
| Gem | Ansatz | Parsen | Letzte Version | Passt für |
|---|---|---|---|---|
| Jbuilder | Template (.json.jbuilder) |
nein | 2.15.1 (05/2026) | Apps, die ohnehin Views nutzen |
| Alba | Ruby-Klasse, schnell | nein | 4.0.3 (11/2026) | frische Projekte, eigenes Format |
| Representable | Decorator, beide Richtungen | ja | 3.3.1 (09/2026) | Ein- und Ausgabe aus einer Definition |
| Roar | Representable plus Hypermedia-Links (u. a. HAL) | ja | 1.2.0 (01/2023) | bestehende Trailblazer-Projekte |
| jsonapi-serializer | JSON:API-Ausgabe | nein | 2.2.0 (03/2021) | JSON:API, aber kaum noch Releases |
| active_model_serializers | Serializer-Klasse | nein | 0.10.16 (12/2025) | Bestandsprojekte |
Jbuilder kommt vom Rails-Team und steht bei rails new ohne --api im Gemfile, im API-Modus nur auskommentiert. Für reine APIs würde ich bei neuen Projekten Alba verwenden: aktiv gepflegt, keine Abhängigkeit von Rails, und die Ressourcen sind normale Ruby-Klassen, die sich leicht testen lassen.
class AuthorResource
include Alba::Resource
attributes :id, :name
end
class ArticleResource
include Alba::Resource
attributes :id, :title
attribute :published_on do |article|
article.published_at&.to_date&.iso8601
end
one :author, resource: AuthorResource
end
ArticleResource.new(article).serialize
# => {"id":42,"title":"…","published_on":"2026-11-09","author":{"id":7,"name":"…"}}
Wer auch Eingaben über dieselbe Klasse abbilden will, sollte bei Representable bleiben. Für das Parsen genügen in vielen Projekten aber Strong Parameters, die du ohnehin verwenden musst.

Ein Representer übersetzt in beide Richtungen: Model zu JSON und JSON zurück ins Model.
JSON:API und HAL: Hypermedia ohne Eigenbau
Irgendwann stellt sich die Frage, wie Beziehungen, Fehler und Links aussehen sollen. Statt eigene Konventionen zu erfinden, kannst du ein fertiges Format übernehmen.
JSON:API (aktuell Version 1.1) legt fest, dass jede Ressource in data mit type, id, attributes und relationships steht. Verknüpfte Datensätze landen in included, Fehler in einem Array errors. Der Medientyp ist application/vnd.api+json. Der Vorteil: Viele Client-Bibliotheken unterstützen das Format direkt, Entwickler auf der Gegenseite können sie ohne eigenen Parser verwenden. Der Preis ist ein recht wortreiches Format.
{
"data": {
"type": "articles",
"id": "42",
"attributes": { "title": "REST in Rails" },
"relationships": { "author": { "data": { "type": "people", "id": "7" } } }
},
"links": { "self": "/api/v1/articles/42" }
}
HAL ist schlanker. Es ergänzt dein JSON nur um _links und optional _embedded. Hypermedia bedeutet hier: Der Server liefert die nächsten möglichen Schritte als Links mit, der Client muss keine URLs selbst zusammenbauen. Im Reifegradmodell von Leonard Richardson ist das Stufe 3 (HATEOAS). Roar bringt für HAL ein eigenes Modul mit. In der Praxis genügt vielen RESTful Diensten schon ein self-Link plus Paginierungslinks.
Sicherheit und Zugriff auf die API
Eine RESTful API ist ohne Browser-Session unterwegs. Clients senden deshalb bei jeder Anforderung ein Token im Authorization-Header. Rails bringt dafür authenticate_or_request_with_http_token mit, das ein fehlendes oder falsches Token einfach mit 401 beantwortet. Seit Rails 7.2 ermöglicht rate_limit to: 10, within: 1.minute außerdem eine Drosselung direkt im Controller, überzählige Aufrufe bekommen 429.
class Api::BaseController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate
private
def authenticate
authenticate_or_request_with_http_token do |token, _options|
@current_client = ApiClient.find_by(token_digest: Digest::SHA256.hexdigest(token))
end
end
end
Für die Sicherheit deiner Daten gilt außerdem: nur HTTPS, Tokens nur als Hash speichern und im Serializer ausdrücklich festlegen, welcher Inhalt nach außen geht. Wer zusätzliche Spalten automatisch ausliefert, veröffentlicht früher oder später etwas, das intern bleiben sollte.
Paginierung: Offset oder Cursor
Eine Liste ohne Begrenzung ist ein Lastproblem, das nur auf den Tag wartet. Zwei Verfahren sind verbreitet:
- Offset:
?page=3&per_page=25wird zuLIMIT 25 OFFSET 50. Leicht zu bauen, Sprünge auf Seite 40 sind möglich. Die Datenbank muss aber alle übersprungenen Zeilen lesen, und kommen zwischen zwei Abrufen Datensätze hinzu, sieht der Client Einträge doppelt. - Cursor (Keyset): Der Client schickt die letzte gesehene ID, die Abfrage lautet
WHERE id > 1234 ORDER BY id LIMIT 25. Mit Index bleibt das auch bei Millionen Zeilen schnell, Seitensprünge gehen dafür nicht.
def index
scope = Article.order(:id)
scope = scope.where("id > ?", params[:after]) if params[:after].present?
articles = scope.limit(26).to_a
next_page = articles.size > 25
articles = articles.first(25)
if next_page
response.headers["Link"] = %(<#{api_v1_articles_url(after: articles.last.id)}>; rel="next")
end
render json: ArticleResource.new(articles).serialize
end
Für Feeds und Synchronisation würde ich Cursor verwenden, für Admin-Tabellen mit Seitenzahlen Offset. Das Gem Pagy kann beide Varianten unterstützen. Wichtig ist, dass der Client die Informationen zur nächsten Seite aus dem Link-Header oder einem links-Objekt übernimmt. Im Beispiel oben setzt der Controller dafür rel="next".
Versionierung, ohne Clients zu brechen
Eine öffentliche API ändert sich, die Apps deiner Nutzer aktualisieren sich aber nicht sofort. Zusätzliche optionale Felder brechen keinen Client. Umbenennen oder Entfernen schon, dafür brauchst du eine weitere Hauptversion. Die meisten Entwickler verwenden in Rails einen Namespace im Pfad, Clients erstellen Ressourcen dann per POST auf /api/v1/articles:
# config/routes.rb
namespace :api do
namespace :v1 do
resources :articles, only: %i[index show create update destroy]
end
end
module Api
module V1
class ArticlesController < Api::BaseController
def create
article = Article.new(article_params)
if article.save
render json: ArticleResource.new(article).serialize,
status: :created, location: api_v1_article_url(article)
else
render json: { errors: article.errors }, status: :unprocessable_content
end
end
private
def article_params
params.expect(article: [:title, :body])
end
end
end
end
params.expect gibt es seit Rails 8. Es antwortet bei falsch geformten Parametern mit 400 statt mit einem 500er. Alternativ lässt sich die Version über den Accept-Header aushandeln, etwa application/vnd.example.v2+json. Das hält URLs stabil, ist im Browser und in Logs aber schlechter sichtbar. Für die meisten Teams ist /api/v1/ die ehrlichere Wahl. Bei internen Diensten, deren Clients du selbst ausrollst, reicht oft eine einzige Version. Weitere Beiträge zu Architektur und Code findest du in der Rubrik Technik.

HTTP-Methode, Rails-Action und Statuscode auf einen Blick.
Checkliste für eine saubere Rails-API
Die folgenden Punkte prüfe ich vor dem ersten Release einer REST-API:
- Ressourcen als Substantive im Plural (
/articles), keine Verben in der URL. - PATCH für Teiländerungen, PUT nur mit echter Ersetzungs-Semantik.
- 201 mit
Location-Header nachcreate, 204 nachdestroy, 422 bei Validierungsfehlern. - Ausgabe über Serializer oder Representer, nie direkt
render json: model. - Ein einheitliches Fehlerformat für alle Endpoints, etwa per
rescue_from ActiveRecord::RecordNotFound. - Listen sollten immer begrenzt sein, Standardgröße 25 bis 100, mit Links zur nächsten Seite.
- Version im Pfad (
/api/v1/) ab dem ersten öffentlichen Client. - Request-Specs, die das JSON-Schema prüfen, damit Änderungen am Serializer auffallen.
- Gem-Stand regelmäßig prüfen: Ein Serializer ohne Release seit Jahren ist ein Risiko beim nächsten Rails-Upgrade.




