The same problem, three different philosophies
Every backend framework eventually needs to answer the same question: how does a database model become the JSON an API actually returns? The three frameworks below solve it with meaningfully different defaults — Django REST Framework is explicit and declarative from the start, Rails offers a quick built-in method that most teams outgrow, and Laravel sits in between with a dedicated Resource class that isn't required until you need it.
Django REST Framework: serializers as the default unit
DRF treats a serializer as a first-class object from day one — there's no "quick" built-in to_json shortcut most teams reach for and later replace; you write a serializer class up front:
from rest_framework import serializers
class OrderSerializer(serializers.ModelSerializer):
customer_name = serializers.CharField(source="customer.name")
class Meta:
model = Order
fields = ["id", "total", "customer_name", "created_at"]
# In a view:
serializer = OrderSerializer(order)
return Response(serializer.data)source="customer.name" reaches across a relationship to pull in a related model's field under a renamed key — a common need (flattening a nested relation into the parent's JSON) that DRF supports directly in the field declaration rather than needing a separate transformation step. Nested serializers (class OrderSerializer containing a CustomerSerializer field) handle the case where you want the full related object embedded, not just one flattened field.
Rails: as_json for quick cases, ActiveModel::Serializer for real APIs
Every ActiveRecord model responds to .as_json out of the box — genuinely the fastest way to get *something* JSON-shaped from a model with zero setup:
order.as_json(only: [:id, :total], methods: [:customer_name])
# => { "id" => 1, "total" => 129.99, "customer_name" => "Ravi Kumar" }This is fine for a prototype or an internal tool, but it doesn't scale well past a few endpoints — the only:/except:/methods: options end up duplicated across every controller action that returns the model, with no single place to see or change "what does an Order look like as JSON." Most production Rails APIs move to ActiveModel::Serializer (or the JSON:API-flavored jsonapi-serializer gem) for exactly the same reason DRF makes serializers mandatory from the start:
class OrderSerializer < ActiveModel::Serializer
attributes :id, :total, :created_at
belongs_to :customer
def customer_name
object.customer.name
end
endLaravel: API Resources as an explicit transformation layer
Laravel's Eloquent models also serialize with zero setup ($order->toJson()), but the recommended pattern for anything resembling a real API is an API Resource class — a dedicated transformation layer between the model and the response:
class OrderResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'total' => $this->total,
'customer_name' => $this->customer->name,
'created_at' => $this->created_at->toISOString(),
];
}
}
// In a controller:
return new OrderResource($order);
// Or a collection:
return OrderResource::collection($orders);The toArray() method is just plain PHP — no special DSL to learn — which makes ad hoc transformations (renaming a field, formatting a date, conditionally including a field based on the authenticated user's role) straightforward to express directly, without needing a framework-specific escape hatch the way some more declarative serializer systems do.
Comparing the three
| Django REST Framework | Rails | Laravel | |
|---|---|---|---|
| Zero-setup option | No — serializer required | Yes — .as_json | Yes — ->toJson() |
| Recommended for real APIs | ModelSerializer (built in) | ActiveModel::Serializer (separate gem) | JsonResource (built in) |
| Nested relations | Nested serializer classes | belongs_to/has_many declarations | Nested Resource classes |
| Renaming a field | source= on the field | Custom method + attributes | Just return a different key from toArray() |
| Collections | many=True on the serializer | Automatic (array of hashes) | ::collection() static method |
The shared lesson underneath all three
Regardless of framework, the pattern that scales is the same: a dedicated transformation layer between the model and the response, not model.to_json() called directly from a controller/view. That layer is what lets you rename a field, hide an internal one, or flatten a relationship without touching every place the model happens to be serialized — and it's exactly the layer that DRF makes mandatory from day one, and that Rails/Laravel both let you skip briefly before every real project reaches for it anyway.