APIs and Backend Logic: Where the Rules Live
· Jerwin Arnado ·
Part two of the full-stack series. The frontend is what users touch; the backend is what they trust. It’s where the rules live — who can do what, what a valid order looks like, how money moves — and the one place you can actually enforce them. A rule that only exists in the browser is a suggestion. This post is how I think about the API layer and structure it in Laravel.
The contract: an API is a promise
An API is a contract between client and server: send a request shaped this way, get a response shaped that way. The discipline is keeping that contract stable and predictable. For HTTP/REST that means using the verbs and status codes the web already defines instead of inventing your own:
| Verb | Means | Status on success |
|---|---|---|
GET |
read, no side effects | 200 |
POST |
create | 201 |
PUT/PATCH |
replace / partial update | 200 |
DELETE |
remove | 204 |
And errors that tell the truth: 401 not authenticated, 403 authenticated but not
allowed, 404 not found, 422 validation failed, 429 rate-limited, 500 you broke.
Clients build real logic on these codes — return 200 with {"error": true} and you’ve
broken the contract.
Validate at the boundary, trust nothing from the client
Every byte from the client is hostile until proven otherwise. Validation isn’t politeness, it’s the wall. In Laravel I push it into a Form Request so the controller never sees bad data:
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:160'],
'body' => ['required', 'string'],
'tags' => ['array', 'max:5'],
'tags.*'=> ['string', 'exists:tags,slug'],
];
}
}
By the time the controller runs, the data is shape-checked and authorized. Two whole classes of bug — bad input and unauthorized access — handled before line one of business logic.
The fat-controller trap
The most common backend mistake: stuffing everything into the controller. It starts innocent and ends as a 400-line method nobody can test. Controllers should be thin — receive, delegate, respond. The actual work lives in dedicated classes:
public function store(StorePostRequest $request, PublishPost $publish)
{
$post = $publish($request->user(), $request->validated());
return new PostResource($post); // shape the output explicitly
}
The controller does three things and stops. PublishPost (an action/service class) owns the
how — saving, firing events, queuing jobs. This is testable in isolation and reusable from
a CLI command or a queued job, not just an HTTP request.
Shape your output on purpose
Returning an Eloquent model straight to JSON leaks your database schema and breaks the moment you rename a column. API Resources give you an explicit output contract:
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'excerpt' => str($this->body)->words(30),
'author' => $this->whenLoaded('author', fn () => $this->author->name),
'published_at'=> $this->published_at?->toIso8601String(),
];
}
}
Now the database and the API can evolve independently. The client depends on the resource, not your table.
Heavy work goes on a queue
Anything slow — sending email, generating a PDF, calling a third-party API — does not belong in the request/response cycle. The user shouldn’t wait for an email to send. Push it to a queue and return immediately:
SendWelcomeEmail::dispatch($user)->onQueue('mail');
The HTTP response comes back fast; a worker handles the slow part out of band. This is the single biggest perceived-performance lever the backend has, and it sets up everything in the later scaling post.
Caveats and best practices
- Never trust the client for authorization. Hiding a button in the UI is not security.
The
403happens on the server or it doesn’t happen. - Version your API (
/api/v1/…) before you have a second client. Breaking changes are inevitable; a version prefix lets old clients keep working. - Keep responses paginated. An endpoint that returns “all” rows works in dev with 12 records and falls over in prod with 120,000. Paginate from day one.
- Idempotency for writes that matter. A double-tapped “Pay” button shouldn’t charge twice. Idempotency keys or dedupe logic save you real money.
Conclusion
Contract → correct verbs + honest status codes
Validate → Form Requests at the boundary, trust nothing
Thin ctrl → receive, delegate to actions, respond
Output → API Resources, never raw models
Slow work → queue it, respond fast
The backend is where correctness is actually enforced, so it pays to keep it disciplined: thin controllers, hard validation, explicit output, async for the slow stuff. Next: database and storage — where all this carefully validated data finally comes to rest.