In smaller apps, you map request fields straight through to your database.
The problem is that this couples your public API to your internal schema. Your consumers see author_id, published_at - database column names that leak implementation details.
Worse, if you ever rename a column or restructure your storage, your API breaks.
If you're not careful, this can also leave you vulnerable to mass assignment, particularly with unguarded models. Decoupling your API fields from your columns gives you an explicit mapping that acts as a natural safeguard.
Consider the following example.
class Post extends Model
{
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
protected function casts(): array
{
return [
'id' => 'integer',
'author_id' => 'integer',
'title' => 'string',
'content' => 'string',
'published_at' => 'date:Y-m-d',
];
}
}
A simplistic approach would allow sending a POST request with fields mapped 1:1:
// POST /posts
{
"author_id": 1,
"title": "Some post title",
"content": "Some post content.",
"published_at": "2026-05-06"
}
And in your controller, you'd take your validated input and pass it straight through to your model.
class PostController
{
public function store(StorePostRequest $request)
{
return PostResource::make(
Post::create($request->validated())
);
}
}
We can start to tighten this up by using cleaner field names in your API, mapping them to your database columns internally.
// POST /posts
{
"author": 1,
"title": "Some other post",
"content": "More post content",
"published": "2026-05-06"
}
We start to minimise our underlying data model exposure here, but have the issue of now needing to map the data back.
There's an argument to be made for where you might put this mapping:
- the form request can work, if you're ok with your "request knowing about your data model"
- you can introduce DTOs which handle the transformation between your public (request) and private (database) models
- for simpler cases, you can handle the mapping in your controller
Whichever approach suits you, we can map between the validated request fields and the database fields quite easily.
class StorePostRequest extends FormRequest
{
public function rules(): array
{
return [
'author' => [
'required',
Rule::exists('users', 'id'),
],
// ...
];
}
public function toFields(): array
{
$map = [
'author' => 'author_id',
'title' => 'title',
'content' => 'content',
'published' => 'published_at',
];
return collect($map)
->intersectByKeys($this->safe()->all())
->mapWithKeys(fn (string $column, string $key) => [
$column => $this->safe()->input($key),
])
->all();
}
}
We can then use the field mapping in our controller
class PostController
{
public function store(StorePostRequest $request)
{
return PostResource::make(
Post::create($request->toFields())
);
}
}
The end result is the same, but now your public API surface is completely decoupled from your data model. You're free to rename columns, restructure your storage, or migrate to a different backend entirely. Your consumers never need to know.
This is a simple example, but the value compounds when you're dealing with legacy field names like uid or post_author, or when your schema evolves over time.
There are ways to handle API versioning in a backwards-compatible way, but the less your public surface depends on your internals, the less versioning you'll need in the first place.
Written by Michael Dyrynda
Principal Engineer, Laravel enthusiast, and open source contributor. I write about web development, PHP, and the problems I solve along the way.