# Client In Tesla, a **client** is an entity that combines middleware and an adapter, created using `Tesla.client/2`. Middleware components modify or enhance requests and responses—such as adding headers or handling authentication—while adapters handle the underlying HTTP communication. For more details, see the sections on [middleware](./2.middleware.md) and [adapters](./3.adapter.md). ## Creating a Client A client is created using `Tesla.client/2`, which takes a list of middleware and an adapter. ```elixir client = Tesla.client([Tesla.Middleware.PathParams, Tesla.Middleware.Logger]) ``` You can then use the client to make requests: ```elixir Tesla.get(client, "/users/123") ``` ### Passing Options to Middleware You can pass options to middleware by registering the middleware as a tuple of two elements, where the first element is the middleware module and the second is the options. ```elixir client = Tesla.client( [{Tesla.Middleware.BaseUrl, "https://api.example.com"}] ) ``` ### Passing Adapter By default, the global adapter is used. You can override this by passing an adapter to the client. ```elixir client = Tesla.client([], Tesla.Adapter.Mint) ``` You can also pass options to the adapter. ```elixir client = Tesla.client([], {Tesla.Adapter.Mint, pool: :my_pool}) ``` ## Single-Client (Singleton) Pattern A common approach in applications is to encapsulate client creation within a module or function that sets up standard middleware and adapter configurations. This results in a single, shared client instance used throughout the application. For example: ```elixir defmodule MyApp.ServiceName do defp client do middleware = [ {Tesla.Middleware.BaseUrl, "https://api.service.com"}, {Tesla.Middleware.BearerAuth, token: bearer_token()}, # Additional middleware... ] Tesla.client(middleware, adapter()) end defp adapter do Keyword.get(config(), :adapter) end defp bearer_token do Keyword.fetch!(config(), :bearer_token) end defp config do Application.get_env(:my_app, __MODULE__, []) end end ``` In this pattern, the client is constructed internally, and operations use this singleton client: ```elixir defmodule MyApp.ServiceName do def operation_name(body) do url = "/endpoint" # The client() function is called internally response = Tesla.post!(client(), url, body) # Process the response... end defp client do # Client construction as shown earlier end end ``` You can then use the module to make requests without managing the client externally: ```elixir {:ok, response} = MyApp.ServiceName.operation_name(%{key: "value"}) ``` ## Multi-Client Pattern In scenarios where different configurations are needed—such as multi-tenancy applications or interacting with multiple services—you can modify the client function to accept configuration parameters. This allows for the creation of multiple clients with varying settings: ```elixir defmodule MyApp.ServiceName do def operation_name(client, body) do url = "/endpoint" # The client is passed as a parameter response = Tesla.post!(client, url, body) # Process the response... end def client(opts) do middleware = [ {Tesla.Middleware.BaseUrl, opts[:base_url]}, {Tesla.Middleware.BearerAuth, token: opts[:bearer_token]}, # Additional middleware... ] Tesla.client(middleware, opts[:adapter]) end end ``` Now, you can create clients with different configurations: ```elixir client = MyApp.ServiceName.client( base_url: "https://api.service.com", bearer_token: "token_value", adapter: Tesla.Adapter.Hackney # Additional options... ) {:ok, response} = MyApp.ServiceName.operation_name(client, %{key: "value"}) ``` ## Comparing Single-Client and Multi-Client Patterns The choice between using a single-client (singleton) or multi-client pattern depends on your specific needs: - **Library Authors**: It's generally advisable to avoid the singleton client pattern. Hardcoding configurations can limit flexibility and hinder users in multi-tenant environments. Providing the ability to create clients with custom configurations makes your library more adaptable and user-friendly. - **Application Developers**: For simpler applications, a singleton client might suffice initially. However, adopting the multi-client approach from the outset can prevent future refactoring if your application grows or needs change. Understanding these patterns helps you design applications and libraries that are flexible and maintainable, aligning with best practices in software development.