Shared Client Pattern
Why Share Clients?
When your provider has multiple resources and data sources, they all need to communicate with the same API. Instead of creating a new client for each resource, share a single configured client across all of them.
Benefits:
- ✅ Single configuration point
- ✅ Reuse HTTP connections (connection pooling)
- ✅ Consistent authentication
- ✅ Better performance
- ✅ Easier testing
The Pattern
1. Provider Struct
type MyCloudProvider struct {
version string
}
2. Configure Method Creates Client
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Parse configuration
var config MyCloudProviderModel
req.Config.Get(ctx, &config)
// Create API client
client := api.NewClient(api.Config{
APIKey: config.APIKey.ValueString(),
Endpoint: config.Endpoint.ValueString(),
})
// Share with resources and data sources
resp.ResourceData = client
resp.DataSourceData = client
}
3. Resources Access Shared Client
func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Get shared client
client := req.ProviderData.(*api.Client)
// Use client to create resource
env, err := client.CreateEnvironment(ctx, ...)
}
Complete Implementation
Provider Definition
package provider
import (
"context"
"os"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mycompany/mycloud-api-client/api"
)
// Provider struct
type MyCloudProvider struct {
version string
}
// Provider configuration model
type MyCloudProviderModel struct {
APIKey types.String `tfsdk:"api_key"`
Endpoint types.String `tfsdk:"endpoint"`
Region types.String `tfsdk:"region"`
Timeout types.Number `tfsdk:"timeout"`
}
// Ensure provider implements interfaces
var _ provider.Provider = &MyCloudProvider{}
func New(version string) func() provider.Provider {
return func() provider.Provider {
return &MyCloudProvider{
version: version,
}
}
}
func (p *MyCloudProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "mycloud"
resp.Version = p.version
}
Provider Schema
func (p *MyCloudProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: "MyCloud provider for managing cloud infrastructure",
Attributes: map[string]schema.Attribute{
"api_key": schema.StringAttribute{
Optional: true,
Sensitive: true,
MarkdownDescription: "API key for authentication. Can be set via MYCLOUD_API_KEY.",
},
"endpoint": schema.StringAttribute{
Optional: true,
MarkdownDescription: "API endpoint URL. Defaults to https://api.mycloud.com",
},
"region": schema.StringAttribute{
Optional: true,
MarkdownDescription: "Default region for resources. Defaults to us-east-1",
},
"timeout": schema.NumberAttribute{
Optional: true,
MarkdownDescription: "Request timeout in seconds. Defaults to 30.",
},
},
}
}
Provider Configure
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// Get configuration
var config MyCloudProviderModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Resolve API key
apiKey := config.APIKey.ValueString()
if apiKey == "" {
apiKey = os.Getenv("MYCLOUD_API_KEY")
}
if apiKey == "" {
resp.Diagnostics.AddError(
"Missing API Key",
"API key must be provided via api_key attribute or MYCLOUD_API_KEY environment variable",
)
return
}
// Resolve endpoint
endpoint := config.Endpoint.ValueString()
if endpoint == "" {
endpoint = os.Getenv("MYCLOUD_ENDPOINT")
}
if endpoint == "" {
endpoint = "https://api.mycloud.com"
}
// Resolve region
region := config.Region.ValueString()
if region == "" {
region = os.Getenv("MYCLOUD_REGION")
}
if region == "" {
region = "us-east-1"
}
// Resolve timeout
timeout := 30
if !config.Timeout.IsNull() {
timeoutFloat, _ := config.Timeout.ValueBigFloat().Int64()
timeout = int(timeoutFloat)
}
// Create API client
client, err := api.NewClient(api.Config{
APIKey: apiKey,
BaseURL: endpoint,
Region: region,
Timeout: time.Duration(timeout) * time.Second,
})
if err != nil {
resp.Diagnostics.AddError(
"Failed to Create Client",
fmt.Sprintf("Could not create API client: %s", err.Error()),
)
return
}
// Test connection
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError(
"Connection Failed",
fmt.Sprintf("Could not connect to API: %s", err.Error()),
)
return
}
// Share client with resources and data sources
resp.ResourceData = client
resp.DataSourceData = client
}
Provider Resources
func (p *MyCloudProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
NewEnvironmentResource,
NewDatabaseResource,
NewNetworkResource,
}
}
func (p *MyCloudProvider) DataSources(ctx context.Context) []func() datasource.DataSource {
return []func() datasource.DataSource{
NewEnvironmentDataSource,
NewDatabaseDataSource,
}
}
Resource Implementation
Resource Struct with Configure
package provider
import (
"context"
"fmt"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mycompany/mycloud-api-client/api"
)
type EnvironmentResource struct {
client *api.Client
}
// Ensure resource implements interfaces
var _ resource.Resource = &EnvironmentResource{}
var _ resource.ResourceWithConfigure = &EnvironmentResource{}
func NewEnvironmentResource() resource.Resource {
return &EnvironmentResource{}
}
Resource Configure Method
func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Prevent panic if the provider has not been configured
if req.ProviderData == nil {
return
}
// Get client from provider
client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *api.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
// Store client for use in CRUD operations
r.client = client
}
Using Shared Client in CRUD
func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var plan EnvironmentModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Use shared client
env, err := r.client.CreateEnvironment(ctx, api.CreateEnvironmentRequest{
Name: plan.Name.ValueString(),
Region: plan.Region.ValueString(),
})
if err != nil {
resp.Diagnostics.AddError(
"Error Creating Environment",
fmt.Sprintf("Could not create environment: %s", err.Error()),
)
return
}
// Update state
plan.ID = types.StringValue(env.ID)
plan.ARN = types.StringValue(env.ARN)
plan.Status = types.StringValue(env.Status)
diags = resp.State.Set(ctx, plan)
resp.Diagnostics.Append(diags...)
}
func (r *EnvironmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state EnvironmentModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
// Use shared client
env, err := r.client.GetEnvironment(ctx, state.ID.ValueString())
if err != nil {
resp.Diagnostics.AddError("Read Error", err.Error())
return
}
// Update state
state.Status = types.StringValue(env.Status)
state.UpdatedAt = types.StringValue(env.UpdatedAt)
diags = resp.State.Set(ctx, state)
resp.Diagnostics.Append(diags...)
}
Data Source Implementation
Data Source Struct
type EnvironmentDataSource struct {
client *api.Client
}
var _ datasource.DataSource = &EnvironmentDataSource{}
var _ datasource.DataSourceWithConfigure = &EnvironmentDataSource{}
func NewEnvironmentDataSource() datasource.DataSource {
return &EnvironmentDataSource{}
}
Data Source Configure
func (d *EnvironmentDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Data Source Configure Type",
fmt.Sprintf("Expected *api.Client, got: %T.", req.ProviderData),
)
return
}
d.client = client
}
Data Source Read
func (d *EnvironmentDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config EnvironmentDataSourceModel
diags := req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
// Use shared client
env, err := d.client.GetEnvironmentByName(ctx, config.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError("Read Error", err.Error())
return
}
// Set data
config.ID = types.StringValue(env.ID)
config.ARN = types.StringValue(env.ARN)
config.Status = types.StringValue(env.Status)
diags = resp.State.Set(ctx, config)
resp.Diagnostics.Append(diags...)
}
Error Handling
Check for Nil Client
func (r *EnvironmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Check if client is configured
if r.client == nil {
resp.Diagnostics.AddError(
"Unconfigured Client",
"Expected configured API client. Please report this issue to the provider developers.",
)
return
}
// Use client...
}
Handle Provider Configuration Errors
func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return
}
client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError(
"Unexpected Resource Configure Type",
fmt.Sprintf("Expected *api.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
)
return
}
r.client = client
}
Testing Shared Clients
Unit Test with Mock Client
func TestEnvironmentResource_Create(t *testing.T) {
// Create mock client
mockClient := &api.MockClient{
CreateEnvironmentFunc: func(ctx context.Context, req api.CreateEnvironmentRequest) (*api.Environment, error) {
return &api.Environment{
ID: "env-123",
Name: req.Name,
Region: req.Region,
Status: "active",
}, nil
},
}
// Create resource with mock client
resource := &EnvironmentResource{
client: mockClient,
}
// Test Create operation
// ...
}
Acceptance Test
func TestAccEnvironmentResource(t *testing.T) {
resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccEnvironmentResourceConfig("test-env"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("mycloud_environment.test", "name", "test-env"),
resource.TestCheckResourceAttrSet("mycloud_environment.test", "id"),
),
},
},
})
}
Advanced Patterns
Multiple Clients
If your provider needs multiple clients (e.g., different API versions):
type ClientBundle struct {
V1Client *api.V1Client
V2Client *api.V2Client
}
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// ... configuration ...
bundle := &ClientBundle{
V1Client: api.NewV1Client(config),
V2Client: api.NewV2Client(config),
}
resp.ResourceData = bundle
resp.DataSourceData = bundle
}
func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
bundle, ok := req.ProviderData.(*ClientBundle)
if !ok {
resp.Diagnostics.AddError("Unexpected Type", "...")
return
}
r.v1Client = bundle.V1Client
r.v2Client = bundle.V2Client
}
Client with Metadata
type ProviderClient struct {
APIClient *api.Client
Version string
UserAgent string
}
func (p *MyCloudProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) {
// ... configuration ...
providerClient := &ProviderClient{
APIClient: client,
Version: p.version,
UserAgent: fmt.Sprintf("terraform-provider-mycloud/%s", p.version),
}
resp.ResourceData = providerClient
resp.DataSourceData = providerClient
}
Best Practices
1. Always Implement ResourceWithConfigure
// ✅ Good: Implement Configure interface
var _ resource.Resource = &EnvironmentResource{}
var _ resource.ResourceWithConfigure = &EnvironmentResource{}
2. Check for Nil Provider Data
// ✅ Good: Check for nil
func (r *EnvironmentResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
if req.ProviderData == nil {
return // Provider not configured yet
}
// ...
}
3. Type Assert Safely
// ✅ Good: Safe type assertion with error
client, ok := req.ProviderData.(*api.Client)
if !ok {
resp.Diagnostics.AddError("Unexpected Type", ...)
return
}
4. Store Client in Resource Struct
// ✅ Good: Store for reuse
type EnvironmentResource struct {
client *api.Client
}
func (r *EnvironmentResource) Configure(...) {
r.client = client
}
5. Test Connection in Provider Configure
// ✅ Good: Test before sharing
if err := client.TestConnection(ctx); err != nil {
resp.Diagnostics.AddError("Connection Failed", ...)
return
}
Common Mistakes
❌ Creating New Client in Each Resource
// Bad: Creates new client every time
func (r *EnvironmentResource) Create(...) {
client := api.NewClient(...) // Don't do this!
}
❌ Not Checking for Nil
// Bad: Might panic if provider not configured
func (r *EnvironmentResource) Create(...) {
env, err := r.client.CreateEnvironment(...) // Panic if r.client is nil!
}
❌ Not Implementing Configure Interface
// Bad: Won't receive shared client
type EnvironmentResource struct {
// Missing Configure method
}
Key Takeaways
- ✅ Configure client once in provider’s Configure method
- ✅ Share client via resp.ResourceData and resp.DataSourceData
- ✅ Implement ResourceWithConfigure and DataSourceWithConfigure
- ✅ Store client in resource/data source struct
- ✅ Check for nil provider data in Configure
- ✅ Use type assertions safely with error checking
- ✅ Test connection before sharing client
- ✅ Reuse HTTP connections for better performance
- ✅ All resources/data sources use same configured client
What’s Next?
Now let’s learn about validating provider configuration.
Next: Configuration Validation →
Previous: ← Configuration Sources
Last updated: June 30, 2026