I used to build backend services the “quick way.” Every function returned something different: sometimes arrays, sometimes strings, sometimes just a boolean. Clients never knew what to expect. Debugging? Nightmare. Error handling? Confusing.
Then I realized I needed structure. That’s when I built a ServiceResponse class. Now, every response is predictable, consistent, and fully traceable.
Here’s what it looks like:
<?php
namespace Usmanzahid\ServiceResponse;
class ServiceResponse {
private ?string $message = null;
private bool $success = true;
private mixed $data = null;
private array $errors = [];
private ?self $previous = null;
public function withMessage(string $message): self {
$this->message = $message;
return $this;
}
public function withSuccess(bool $success): self {
$this->success = $success;
return $this;
}
public function withError(string $key, string $message): self {
$this->errors[$key][] = $message;
return $this;
}
public function withErrors(array $errors): self {
foreach ($errors as $key => $messages) {
foreach ((array) $messages as $message) {
$this->withError($key, $message);
}
}
return $this;
}
public function withData(mixed $data): self {
$this->data = $data;
return $this;
}
public function withPrevious(self $previous): self {
$this->previous = $previous;
return $this;
}
public function getMessage(): ?string {
return $this->message;
}
public function wasSuccessful(): bool {
return $this->success===true;
}
public function wasNotSuccessful(): bool {
return !$this->wasSuccessful();
}
public function getErrors(): array {
return $this->errors;
}
public function getAllErrors(): array {
$all = $this->errors;
$prev = $this->previous;
while ($prev) {
$all = array_merge_recursive($prev->getErrors(), $all);
$prev = $prev->previous;
}
return $all;
}
public function getData(): mixed {
return $this->data;
}
public function getRootCause(): self {
$origin = $this;
while ($origin->previous!==null) {
$origin = $origin->previous;
}
return $origin;
}
public function toArray(): array {
return [
'success' => $this->wasSuccessful(),
'message' => $this->message,
'data' => $this->data,
'errors' => $this->getAllErrors(),
];
}
}
Why It Changed Everything
1. Consistency
Every response now has the same shape: success
, message
, data
, errors
. No surprises.
2. Chained Methods
Build responses step by step with fluent syntax. For example:
return (new ServiceResponse())
->withSuccess(false)
->withMessage('Something went wrong')
->withError('email', 'Invalid email address');
3. Error Aggregation
Multiple errors? Nested errors? Fully traceable. I finally know why something failed, not just that it did.
4. Flexible Data
Any payload can be returned—strings, objects, arrays—while keeping the response structure intact.
5. Root Cause Tracking
If an error bubbles up through multiple layers, the original source is always available. Debugging becomes easier, and logs actually make sense.
The Takeaway
What started as a small class to standardize API responses turned into a tool that made my backend predictable, debuggable, and professional. No more random return types. No more guessing. Now every request and response has full context, and handling errors feels natural.
— Usman Zahid
Top comments (0)