Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ChrisF79's avatar

Should this data from an API call should be in a model or a controller?

I'm accessing data from a real estate API. It's typical things like # of bedrooms, price, bathrooms, property type, property description, etc.. Right now, I'm grabbing the data from the API within this controller pasted below. However, as a completely new programmer, this doesn't feel right. Should it be in a model instead?

All of those random echo statements in there are simply for me to debug quickly. This will ultimately be nicely formatted in a blade file.

What is the best practice?

0 likes
8 replies
LaryAI's avatar

Great question! You're right to feel that something is "off" about putting all your API logic in the controller. In Laravel (and most MVC frameworks), controllers should be kept as thin as possible—their job is to handle requests and responses, not to fetch or process data directly.

Best practice:
Move your API-fetching logic into a dedicated class. In Laravel, this is often done in a "service" class, or sometimes in a model if the data maps closely to a database table. Since your data comes from an external API (not your own database), a service class is most appropriate.


How to Refactor

1. Create a Service Class

Create a new file at app/Services/PropertyApiService.php:

<?php

namespace App\Services;

use GuzzleHttp\Client;

class PropertyApiService
{
    protected $client;
    protected $apiKey;
    protected $baseUrl;

    public function __construct()
    {
        $this->client = new Client();
        $this->apiKey = '0123456789';
        $this->baseUrl = 'https://api.blahblahblah.com/api/v2/OData/nabor/Property';
    }

    public function getPropertyByListingId($listingId)
    {
        $select = '$select=AccessibilityFeatures,Appliances,ArchitecturalStyle,...'; // (shortened for clarity)
        $filter = "\$filter=ListingId eq '$listingId'";
        $url = "{$this->baseUrl}?access_token={$this->apiKey}&{$filter}&{$select}";

        $response = $this->client->get($url);
        return json_decode($response->getBody(), true);
    }
}

(Fill in the full $select string as needed.)


2. Use the Service in Your Controller

Update your controller to use this service:

namespace App\Http\Controllers;

use App\Services\PropertyApiService;

class PropertyController extends Controller
{
    protected $propertyApi;

    public function __construct(PropertyApiService $propertyApi)
    {
        $this->propertyApi = $propertyApi;
    }

    public function getProperty()
    {
        try {
            $data = $this->propertyApi->getPropertyByListingId('225037268');
            return view('property', ['propertyData' => $data]);
        } catch (\Exception $e) {
            return view('api_error', ['error' => $e->getMessage()]);
        }
    }
}

3. Bind the Service (Optional)

If you want to use dependency injection (as above), Laravel will auto-resolve the service if you use the default service provider structure.


Summary

  • Controllers: Handle HTTP requests, pass data to views.
  • Service classes: Handle API calls, business logic, and data processing.
  • Models: Represent database tables (not needed here unless you want to store API data locally).

Your controller should be thin and delegate the API logic to a service class.
This makes your code easier to test, maintain, and extend.


Extra:
If you want to keep things even more organized, you can create a DTO (Data Transfer Object) or ViewModel to format the API data before passing it to the view, but that's optional for now.


In summary:

Move the API-fetching logic out of your controller and into a dedicated service class.
Keep your controller focused on handling requests and returning views.

Let me know if you need a more detailed example or have questions about dependency injection!

ChrisF79's avatar

I don't have a "Services" directory under app. Am I supposed to just create that?

martinbean's avatar
Level 80

@chrisf79 First things first, remove the try/catch. It’s completely useless if an exception does occur, because you’re just catching it, outputting the message to the user, but then it’s not logged anywhere. So if an exception does occur, and the user doesn’t tell you and instead leaves your site because it’s broken, you’re never going to know, because those exceptions are never going to show up in an error log.

Getting back to the original question, it depends on what your actual goal is. If it’s to show properties based on data from an external API, then I’d be moving that logic out to a dedicated class. Your controller should then just call a method on this class to retrieve details of a property based on an identifier:

class PropertyController extends Controller
{
    protected PropertyService $properties;

    public function __construct(PropertyService $properties)
    {
        $this->properties = $properties;
    }

    public function show(string $id)
    {
        $property = $this->properties->retrieve($id);

        return view('property.show', compact('property'));
    }
}

The advantage to this is, that logic is now self-contained, which is ideal as you definitely want to implement caching if you‘re getting your data from a third party API. If someone is hitting the same property page multiple times a minute, you don’t want the lag of making an API request each and every time that property is requested. Fetch it once, cache the details for a predetermined amount of time (i.e. 10 minutes), and then you won’t be getting rate-limited or just flat-out blocked for making too many requests in too short a time.

I’d also have a class that represents a “property” in your application, and map the API response data to that class. This means you can define what a property looks like in your application. So your service class could look something like this:

The property class would just take the response data from the API as an array, and map the data to properties on the class:

namespace App\Entities;

class Property
{
    public readonly Address $address;
    public readonly int $bedrooms;
    public readonly int $bathrooms;
    public readonly array $imageUrls = [];
    // And so on...

    public function __construct(array $response)
    {
        $this->address = new Address(
            streetNumber: $response['StreetNumber'],
            streetName: $response['StreetName'],
            locality: $response['City'],
            region: $response['StateOrProvince'],
            postalCode: $response['PostalCode'],
        );
        $this->bedrooms = $response['BedroomsTotal'];
        $this->bathrooms = $response['BathroomsTotalInteger'];

        foreach ($response['Media'] as $media) {
            $this->imageUrls[] = $media['MediaURL'];
        }

        // And so on...
    }
}

Extracting this to a class gives you two benefits:

  1. You can now wrap it in caching.
  2. You can mock calls to the service in tests.

So, to implement caching, just can cache the property data using the property ID:

$key = sprintf('property.%s', $id);
$ttl = Date::now()->addMinutes(10);

return Cache::remember($key, $ttl, function () {
    // Get data from API...

    return new Property($response->json());
});

You can now also test your controller by mocking the service class method:

public function test_can_view_property_details(): void
{
    $property = new Property([
        'Bedrooms' => 2,
        'Bathrooms' => 1,
        // And so on...
    ]);

    $this->mock(PropertyService::class, function (MockInterface $mock) use ($property): void {
        $mock->shouldReceive('retrieve')->once()->with('TEST_ID')->andReturn($property);
    });

    $this
        ->get('/properties/TEST_ID')
        ->assertOk()
        ->assertViewIs('property.show')
        ->assertViewHas('property', $property);
}
1 like
ChrisF79's avatar

@martinbean Thank you so much for taking the time to write this. Just a few questions if you don't mind as I'm really trying to learn and understand.

  1. PropertyService is my controller and Property is a model in this correct?
  2. Can you describe what public readonly Address $address; does and the new Address? Are we just creating an object with that code?
  3. For a site that isn't going to get a ton of traffic, is it still worth it to cache results? Is that in one of my existing controllers?

Thank you again. I'm amazed you took that much time to answer me!

Snapey's avatar

@ChrisF79

Can you describe what public readonly Address $address; does and the new Address? Are we just creating an object with that code?

$public $address; you probably recognise as creating a property that is called $address

Address means that the property can only hold an object of type Address

readonly means that despite it being a public property, no-one is allowed to change its value.

martinbean's avatar
  1. PropertyService is my controller and Property is a model in this correct?

@chrisf79 No. PropertyService is a separate class. Your property controller would still be called PropertyController, and the Property class is just that: a class. It’s not an Eloquent model. You can create plain PHP classes in Laravel applications; not every class needs to fit into a pre-prescribed Laravel class such as a model.

  1. Can you describe what public readonly Address $address; does and the new Address? Are we just creating an object with that code?

It’s just PHP syntax.

  • The public keyword means it’s a public property, so accessible like $property->address
  • The readonlykeyword means the property’s value can only be read, and not assigned to (so$property->address = 'foo'` would not work)
  • The Address is just a type-hint, saying that the value of the $address property is an instance of a class named Address (I didn’t include the definition in my comment)
  1. For a site that isn't going to get a ton of traffic, is it still worth it to cache results? Is that in one of my existing controllers?

Yes. I explicitly mentioned caching in my comment:

The advantage to this is, that logic is now self-contained, which is ideal as you definitely want to implement caching if you‘re getting your data from a third party API.

1 like
GdS's avatar

Very solid advice from @martinbean

Alternatively, if you are managing multiple API integrations I would consider Laravel Saloon because it offers a structured approach to follow and a handful of nice helpers out of the box.

martinbean's avatar

@GdS Even with Saloon, you’d still need a way to map responses from whatever API, to an entity class within the application. So if @chrisf79 did want to support fetching property data from multiple APIs, then they could create multiple service classes that adhere to a common interface, but then this is getting away from the actual question they asked.

Something I did miss in my comment, was mentioning about the access token being injected into the service class. This is assuming that the access token is in a configuration value, and you can use a service provider to automatically instantiate the service class and pass in the access token:

public function register(): void
{
    $this->app->singleton(PropertyService::class, function () {
        return new PropertyService(config('services.bridge.access_token'));
    });
}

Now you can just type-hint PropertyService in classes that need it (i.e. controllers) without having to manually instantiate and pass the access token each and every time you need an instance of the service class.

1 like

Please or to participate in this conversation.