After deciding where components and composables belong, the next question naturally becomes:
Where should API communication live?
This is one of those decisions that seems unimportant when a project is small.
Fetching data directly inside a component works.
Putting API requests inside composables, stores, or utility files works too.
The problem isn't whether those approaches work.
The problem is consistency.
As an application grows, API requests slowly become scattered throughout the codebase. Some live in components, others in composables, stores, or helper functions. Eventually, finding where a request actually happens becomes harder than writing it.
Over the years, I've settled on one simple principle:
Services own external communication.
Everything else builds on top of that.
In this article, I'll explain why I keep API logic inside services, what I believe belongs there, and how that separation keeps large Vue applications easier to understand, test, and maintain.
Why do API requests become scattered?
Just like components/ and composables/, API communication rarely becomes messy overnight.
It usually starts with a single request inside a component.
Eventually another component needs the same data.
The request gets copied.
Later a composable needs it.
Then a store.
Before long, the same responsibility exists in multiple places.
The problem is that communication no longer has a clear owner.
One endpoint is called from a component.
Another lives inside a composable.
A third is buried inside a store.
Maybe there's even a helper somewhere making requests too.
Finding where a request happens becomes harder than writing it.
Changing an endpoint, updating authentication, or debugging an issue now means searching through the project instead of going directly to one obvious place.
The real problem
This isn't an HTTP problem.
It's an ownership problem.
Just like components and composables, API communication should have one clear owner.
For me, that owner is always the service.
So before I create any new request, I don't ask:
"Where can I call this API?"
Instead, I ask something much more important:
"Who owns communication with this external system?"
That single question determines almost every decision I make.
Instead of thinking about convenience, I think about responsibility.
Every API request should have one clear owner.
For me, that owner is always the service.
Once that responsibility is clear, deciding where a request belongs becomes almost automatic.
Why components shouldn't fetch
A component has one primary responsibility:
Present the UI and respond to user interactions.
Making HTTP requests is a completely different responsibility.
When a component owns both presentation and communication, those concerns become tightly coupled.
Instead of simply describing how the interface behaves, the component now also knows where the data comes from and how to retrieve it.
That might not seem like a problem when there's only one request.
But applications rarely stay that small.
Eventually, another component needs the same data.
Now you have two choices:
- duplicate the request
- extract it somewhere else
Instead of starting with a clear architecture, you're now refactoring because of duplication.
By keeping API communication inside a service from the beginning, every component talks to the same abstraction.
The component doesn't care whether the data comes from a REST API, GraphQL endpoint, local cache, or something else.
Its job is simply to render the result.
Here's what that separation looks like in practice:
Notice that the component doesn't know anything about endpoints, Axios, authentication, or request configuration.
It simply asks the service for the data it needs.
Why composables shouldn't fetch directly
Unlike components, composables are designed to encapsulate reusable reactive behavior.
Managing state, coordinating multiple pieces of logic, or exposing a reusable API to components are all great reasons to create one.
But making HTTP requests is still a different responsibility.
A composable shouldn't need to know how data is retrieved.
It should only know when it needs data and what to do with it.
Instead of communicating with an API directly, a composable should delegate that responsibility to a service.
The component owns the UI and decides when the data is needed.
The service owns how that data is retrieved.
Each layer stays focused on a single responsibility.
That's exactly the same principle we've followed throughout this series.
The same idea also applies to composables.
Even though they're designed to encapsulate reusable logic, I don't believe they should communicate with APIs directly either.
What belongs in a service?
For me, a service has one responsibility:
Communicate with external systems.
┌────────────────────────────┐ ┌────────────────────────────┐
│ Service Owns. │ │ Doesn't Own. │
├────────────────────────────┤ ├────────────────────────────┤
│ HTTP Requests │ │ ref() │
│ Endpoints │ │ computed() │
│ Query Parameters │ │ Loading State │
│ DTO Mapping │ │ Dialogs │
│ Request Configuration │ │ Router │
│ External Communication │ │ Notifications │
└────────────────────────────┘ └────────────────────────────┘
Nothing more.
That includes:
- making HTTP requests
- defining endpoints
- configuring request parameters
- transforming responses into application models (or delegating to mappers)
- handling request-specific concerns
What doesn't belong in a service is just as important.
A service shouldn't know anything about the user interface.
That means no:
- ref()
- computed()
- loading state
- dialogs
- notifications
- router navigation
Those belong to the layer that's consuming the service.
Keeping that boundary clear makes services reusable from components, composables, stores, or even server-side code without bringing UI concerns along with them.
Services vs Composables
Services and composables solve different problems.
A service answers:
"How do I communicate with an external system?"
A composable answers:
"How do I coordinate reusable reactive behavior?"
A composable may call one or more services.
A service should never know a composable exists.
Think of it this way:
- Services communicate.
- Composables coordinate.
The architecture in practice
At this point, every layer has a well-defined responsibility.
Components focus on rendering the UI.
Composables coordinate reusable reactive behavior.
Services communicate with external systems.
Each layer builds on top of the one below it without taking over its responsibility.
Instead of every layer knowing how to talk to an API, there is a single, predictable path for data to flow through the application.
That makes the architecture easier to understand, easier to test, and much easier to extend as the project grows.
Components render. Composables coordinate. Services communicate.
One composable may orchestrate multiple services.
A service, however, should never know that a composable exists.
Conclusion
Throughout this series, we've been asking the same question from different angles.
- Where should this file live?
- Who owns this component?
- Who owns this logic?
- Who owns API communication?
The answer has always been the same:
Every responsibility should have a clear owner.
When ownership is clear, architecture becomes predictable.
And when architecture is predictable, software becomes easier to build, test, and maintain.
That's the principle I try to follow in every Vue application I build.




Top comments (0)