What you are doing is rather onion architecture or clean architecture, but it is definitely not hexagonal architecture.
The idea of isolating the domain layer from the infrastructure layer using an adapter pattern (with common adapters templates such as a Repository) is not hexagonal architecture. The very concept of domain as a unit of architecture was made popular by Evan's book on DDD 3 years after Alistair Cockburn coined the concept of hexagonal architecture.
Hexagonal architecture
In the ports folder, I have an interface that, according to hexagonal architecture, is responsible for connecting to the infrastructure layer.
No, hexagonal architecture - if that's what you actually want to implement - states that the port is responsible for connecting to the infrastructure itself, not the infrastrucure layer.
A port isolates your application from code or technology that is beyond your control.
By doing so, it allows you to decide whether you want to actually call that external reference or not in your context.
This is why a port is defined as close as possible to 'technology': external libraies, network operations, system operations, filesystem, ...
In your case, your application has two ports:
- When defering the http calls to axios;
- When accessing the localStorage.
Since TypeScript supports structural typing, you don't even need to define interfaces for your ports.
As long as you are able to inject somehow mocked axios or mocked localStorage objects, you're doing hexagonal architecture.
However, creating an interface type can be a simple mean of creating and injecting mocks depending on your testing framework.
Hexagonal architecture also do not require you to write adapters.
This is only needed if you don't like one of your externals interface.
It allows you to create a port with a different structure, and a small set of instructions to convert from/to the actual one.
It should be as small as possible as it is code that you write (and thus a mean of making your application fail) although usually not being in the system under test.
How you implement the rest of the application is beyond the scope of hexagonal architecture.
Clean/Onion architecture
I don't really understand why there are so many files to generate, for example, in the use cases. From the hook, I call the use cases, but I could also call the methods of taskService.
If your question was actually about the correct implementation of onion/clean architecture, rather than hexagonal per se, one aspect of the answer is that you are mixing patterns in your example.
However, another important aspect of why it feels awkward is because your example is not a good example to practice onion/clean.
Your application manages todo objects, but these todo object are simple data bags without any business/domain logic.
In consequence, your app is a mere CRUD of objects, and it makes it hard to implement architectural patterns based on domain-driven design when you don't have a domain logic.
A striking example of the problem is that you have an "udate todo use case". Updating and object is not domain-driven, it's crud-driven.
A much better example of a domain-driven use case would be to "reschedule a task", with perhaps some validation rules. For instance: "the new target date cannot be in the past".
The service classes are IMHO a common antipattern of trying to separate logic from data in the domain. It should be the other way around.
Sorry I don't speak TypeScript very good, but here is an example in C#:
public class Task
{
public DateTime Due { get; private set; }
public bool Reschedule(DateTime to)
{
if(to < DateTime.now) { return false; }
Due = to;
return true;
}
}
Then since the domain class should not know about the repository, hence the need for a RescheduleTaskUseCase class in the application layer:
public class RescheduleTaskUseCase(ITaskRepository repository)
{
public Exception? Execute(Guid taskId, DateTime newDate)
{
if(repository.Find(taskId) is not Task task) { return new NotFoundException(); }
if(!task.Reschedule(newDate)) { return new InvalidOperationException(); }
repository.Update(task);
return null;
}
}
Now your app is free to implement the ITaskRepository however it see fit.
Congratulations, you have now abstracted your infrastructure needs at the domain level (ITaskRepository) from it's actual implementations in the infra layer: LocalStorageRepository, AxiosRepository, RedisCached<AwsS3Repository>, ...
These implementations, as they transfer the responsibility of actually interacting with third party code or infrastructure, COULD use an hexagonal architecture to isolate the actual handoff.
It would keep your repository implementation in the system under test if it has logic (for instance, session management, etc ...).
However, if the implementation is a simple localStorage.Get(id), you could also consider the repository's interface as your hexagonal port.
Where should I manage whether the calls were successful or if there was an error?
What happens depending on whether the use case was successful or failed is a prsentation concern: it should be done in the UI layer: notify success or failure to the user, redirecting navigation to another page, etc ...
If there are other 'infrastructure' concerns (logging failures, sending events, ...) should be included in the use case.
Where should I use Zod to check that the received data matches the interface?
This is an implementation detail of your repository class. It should go in the infrastructure layer.
Conclusion
As you can see, the purpose of both patterns are quite different. Hexagonal is about isolating your whole application from third party technology ('below' the infra layer), whereas onion/clean try to isolate the domain logic from the infra concerns.