DEV Community

Cover image for Service Responses | From Chaos to Clean APIs
Usman Zahid
Usman Zahid

Posted on

Service Responses | From Chaos to Clean APIs

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(),
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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)