Skip to content

How to create my own client? #671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
douglasg14b opened this issue Jun 11, 2024 · 48 comments · Fixed by #1889
Closed

How to create my own client? #671

douglasg14b opened this issue Jun 11, 2024 · 48 comments · Fixed by #1889
Labels
feature 🚀 New feature or request

Comments

@douglasg14b
Copy link

douglasg14b commented Jun 11, 2024

Description

This is similar to #425

We need to be able to pass envs to our custom client, and even wrap the API services within the client itself so they are accessible as properties.

Are there docs or examples outlining the best ways to go about this? The deprecation docs are unclear on how clients replaces or supplements those options. or how one goes about constructing their own custom client 🤔

The surface area is awkward in that the API calls are not pure, they leak the abstraction back up to the caller who has to specify a body property on their options. It would be nice to expose a more pure client SDK.

Thanks!

@douglasg14b douglasg14b added the feature 🚀 New feature or request label Jun 11, 2024
@douglasg14b douglasg14b changed the title How to create my own client that users instantiate? How to create my own client? Jun 11, 2024
@douglasg14b
Copy link
Author

douglasg14b commented Jun 11, 2024

Honestly, just giving library consumers the ability to pass in an env or client config themselves to define the baseUrl would be enough to get rolling. Is this possible without a custom client?

Similar to what we see here: https://github.com/ferdikoomen/openapi-typescript-codegen/wiki/Client-instances

Or if there is a straight forward way to wrap the client and set it's global config at runtime?

@mrlubos
Copy link
Member

mrlubos commented Jun 12, 2024

@douglasg14b are you able to show roughly what kind of API you're hoping to use?

@Nick-Lucas
Copy link

We also make heavy use in my team of the "name" option, now with asClass, to generate client classes for each of our APIs.

It's very useful because we can new up and inject client instances into our BFF, and also pass around services polymorphically.

It hasn't been entirely clear to me what the deprecation notice for "name" is about, since at face value that's the only way to generate a instantiable client for injection or connect to 2 instances of the same API within one application - mutating the global OpenApi object to configure the API is a step backwards in that regard

@mrlubos
Copy link
Member

mrlubos commented Jun 12, 2024

@Nick-Lucas We will want to handle those use cases for sure. The main issue with name is it's not obvious when to use it/what it does. It's a completely different way of generating clients with its own set of problems. And if you have two clients, they're 99% identical. There will be a cleaner approach to this, it just hasn't been prioritised

@Nick-Lucas
Copy link

@Nick-Lucas We will want to handle those use cases for sure. The main issue with name is it's not obvious when to use it/what it does. It's a completely different way of generating clients with its own set of problems. And if you have two clients, they're 99% identical. There will be a cleaner approach to this, it just hasn't been prioritised

Understood! Well if I can help to figure this out I'll be happy to

Right now setting name: 'MyClient', services: { asClass: true } is working fine as before, though if you don't enable asClass it's bugged right now and imports a non-existent DefaultService for the class. That probably deserves its own bug ticket though

@mrlubos
Copy link
Member

mrlubos commented Jun 12, 2024

Pretty sure someone reported it already, there might be an issue open. This illustrates perfectly the challenge of maintaining such a large API surface. I wanted to bring that feature in, but adding it to every configuration would take way longer and I know I want to rework "named" clients, so I didn't bother spending way more time on adding it to them

@douglasg14b
Copy link
Author

douglasg14b commented Jun 12, 2024

are you able to show roughly what kind of API you're hoping to use?

yeah!

Essentially my goal is an API client that's namespaced, the user can pass in options once, and can reference an instantiated version of.

// Somewhere

export const someApiClient = createSomeApiClient({ baseUrl: '' });


// elsewear:


import someApiClient  from '@/something/client';`

...

await someApiClient.feature.endpoint();

And it by type safe throughout.


I have achieved that with:

import * as services from './services.gen';
import type { CamelCase } from 'type-fest';

type RemoveServiceSuffix<S extends string | number | symbol> = S extends `${infer Prefix}Service` ? Prefix : S;

type ApiClient<T extends object> = {
    [Service in keyof T as CamelCase<RemoveServiceSuffix<Service>>]: T[Service];
};

type ApiClientOptions = {
    baseUrl: string;
};

/** This creates a client SDK from the generated services, adding them as properties to the client and automatically filling in the provided baseUrl */
const createApiClient = <T extends object>(services: T, options: ApiClientOptions): ApiClient<T> => {
    const apiClient = {};

    for (const serviceName in services) {
        const serviceClass = (services as any)[serviceName];

        const serviceClient: { [methodName: string]: any } = {};
        for (const methodName of Object.getOwnPropertyNames(serviceClass)) {
            if (methodName !== 'constructor' && typeof (serviceClass as any)[methodName] === 'function') {
                serviceClient[methodName] = (args) => {
                    return (serviceClass as any)[methodName]({ path: args?.path, body: args?.body, ...options });
                };
            }
        }

        // Convert PascalCase to camelCase for the service name, remove "Service" from name
        const camelCaseServiceName = serviceName.charAt(0).toLowerCase() + serviceName.slice(1).replace(/Service$/, '');
        apiClient[camelCaseServiceName] = serviceClient;
    }

    return apiClient as ApiClient<T>;
};

export type CreateClientOptions = {
    baseUrl: string;
};

export function createClient({ baseUrl }: CreateClientOptions) {
    return createApiClient(services, { baseUrl });
}

This is of course, a bit of a hack given all the any casting, however it works since we separate the type definition from the actual runtime client object. anyways.

@douglasg14b
Copy link
Author

douglasg14b commented Jun 12, 2024

Now that I've done this, there are a few things this lib could do to make such extensibility easier:

  1. An option to bundle all the services & endpoints into a single object
  2. An option to not use static methods
  3. Option to have a different codegen rollup file other than index.ts (Which is generated so I can't export this client from it)

A bundled up "client" that can be instantiated would do essentially what this does

Bonus:

  • Ability to generate a package.json & all the bits necessary for package distribution. (Separate thing TBH)

@omridevk
Copy link

omridevk commented Jun 12, 2024

@douglasg14b
Using the new fetch client, I was able to create a sort of my own client that uses either "Got" or "Ky"

import {createClient as createFetchClient} from '@hey-api/client-fetch'


export async function createClient(
  options: Parameters<typeof createFetchClient>[0] & {httpClient: Got | KyInstance},
) {
  const {httpClient} = options
  const client = createFetchClient({
    ...options,
    fetch: createFetch(httpClient),
  })
  return client
}

export function createFetch(client: Got | KyInstance) {
  return async (request: Request) => {
    const result = await client[request.method?.toLowerCase() as 'get'](request.url.toString(), {
      throwHttpErrors: false,
      headers: normalizeHeaders(request.headers) as unknown as Headers,
      ...(request.body && {json: await request.json()}),
    }).json()
    const response = new Response(JSON.stringify(result), {})
    return response
  }
}

this is just a POC but it seems to be working, there are probably some edge case, but the idea is that you can use the new fetch client and just provide your own fetch implementation

@omridevk
Copy link

I can of course pass a custom base URL to my fetch implementation

@mrlubos
Copy link
Member

mrlubos commented Jun 12, 2024

That's pretty cool @omridevk. Still think we need a separate client for those?

@omridevk
Copy link

nope, this solution seems to be good enough at least for our needs.

@omridevk
Copy link

also created this PR:
https://github.com/7nohe/openapi-react-query-codegen/pull/125/files
so we can use this version with react-query

@Nick-Lucas
Copy link

I'm actually unclear how what's being described here is different from setting name and asClass. You already get a named client class with all the services attached as class instances, and can pass all your options in to the constructor.

@bombillazo
Copy link

Hey, im having a similar question, the lib generates an export const client = createClient... which seems to be a singleton object to be used as the client object, and you can call client.setConfig to update/modify the client headers, etc.

My question is, in a server env where memory is shared in a long running process, couldn't 2 or more separate requests that want to use the client call setConfig and execute request with the wrong data?

@bombillazo
Copy link

other libraries either allow you to instantiate a new class instance with the config parameter in the constructor, or they provide a factory function that creates another client object with the setup you pass to the factory function.

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

@bombillazo my assumption was that you'd call setConfig() only once when you start your application. Can you provide more details when would "2 or more separate requests that want to use the client call setConfig and execute request with the wrong data?" The generated requests never call setConfig(), it would have to be done by the user. If that's what you're doing and calling setConfig() multiple times, I'd like to know why?

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

We're using openapi-ts to build an SDK for 3rd party APIs, to which we send requests using different accounts from our API backend. When we need to call the 3rd party API, we want to use the generated SDK client, but we need to pass context-specific data (like the authorization headers) that depend on the request itself, so it is not static across the application. With other pre-existing SDKs , we'd simply instantiate an SDK client in our logic where needed and pass the headers in the constructor. Any subsequent logic uses that client for the remainder of the request execution to call the 3rd party. Since each request has its instance, there are no issues with cross-config requests due to sharing the same client.

With a singleton pattern, we may run the risk of one request setting the config and another one coming in and calling setConfig, and both requests use the client configured by the last request. This forces us to pass this header data every single time on every single method we use the SDK client for since we cannot set it at the client level. As you can imagine, that isn't fun, and the code becomes very WET.

@Nick-Lucas
Copy link

As here you can generate an instantiable client already to solve the problem, my own contribution to the thread is just about some deprecations around the current API

#671 (comment)

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

Thanks @Nick-Lucas. To your earlier comment, I think it's not bugged anymore RE asClass, correct? It should be having no effect with name.

@bombillazo can you confirm your use case is for server-to-server communication? If you're using the generated SDKs on the client, is there ever a concern about the size of the generated bundle?

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

We're using it server to server, in our case bundle size isn't a huge concern.

I understand now why it is the way it is right now, based on your assumption.

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

@bombillazo let me know if using name works for you. It is marked as deprecated, but won't be removed before there's a clear way forward. It is a feature from the previous package https://github.com/ferdikoomen/openapi-typescript-codegen and until now the focus was on client-side codegen (hence tree-shaking etc), but full SDKs will receive some love at some point too

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

Okay, got it. Yeah, the only path forward, for now, is to pass options with the headers on every API method call to ensure the client uses the correct data.

If the codegen eventually generates code that allows for multiple client instances of the same SDK, believe me, that will be a game changer for backend development.

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

Any chance you've got a repository I could play with to verify the issues and test any potential solutions when I get around to it?

@bombillazo
Copy link

Unfortunately no, but we're using Hono for our backend server.

I only anticipated the issue after looking at the produced code, it runs without problems but I noticed there was a single instance of the client which can be a problem in server environments.

Some solutions that come to my mind require what I mentioned earlier: either generate a FullAPI class that envelopes all the API services with a constructor that can be used to set the client options and have the current client be an internal property of the FullAPI, and one can call new FullAPI() (as an example name).

Or have a function that somehow creates and returns a new object that can store the config internally and has access to all the API methods. This one sounds less performant since depending on how its done would require to mount the object every time with teh API function methods and setting up the common config to use.

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

yeah I think new MyApiClient() would be the way to go, I just didn't get around to it. Based on the feedback I receive, I assume most people are using this package client-side. Of course, if it's optimised for one use case, there's a low chance it will attract other user types. Maybe there should be a separate issue for full SDKs to gauge interest.

@bombillazo
Copy link

Right now the code generates an index.ts file with an exported const object called client created with the createClient function.

All API methods function generated (both class and flat) are internally pointing to that const object . When the server app runs, it'll create the singleton (that specific instance object) and no matter where we import the client from in our logic, it'll use that same object.

So the code gen solution would be a whole different approach from Front End where each app essentially runs "isolated" on each browser tab and that client isn't really shared and is only used within the context of that app "session" .

@bombillazo
Copy link

I'm willing to help and contribute too. I'm interested in this library, so far I've liked what I've seen.

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

Your description checks out. As it is, are you blocked at using the generated output?

@Nick-Lucas
Copy link

Have you tried setting "name" and checking the output of that?

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

@mrlubos Not blocked perse, we can still use the client passing headers on every single method call, fetch will set the headers individually as expected.

@Nick-Lucas Hey, how would changing the name of the client help?

@Nick-Lucas
Copy link

It generates a named and instantiable client, a class you can new MyName() on and pass instance options like headers

It's an older option from the original library which hasn't been adapted yet to the new codegen approach but I use it at work for holding several instances

@bombillazo
Copy link

Oh, let me try it out, but if it continues to internally use a client object singleton I doubt it'll solve the problem. I'll confirm.

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

@Nick-Lucas Wow, I think that is a solution in the right direction; it essentially creates the instantiable wrapper class I alluded to previously as a potential solution. It does the configuring of each internal service with the initial config.

However, I see various issues on how it currently outputs:

  • It doesnt generate the core folder for me, so some imports are broken.
  • The SDK constructor is broken, like it wants to assign a var to the HttpRequest param with = but has no value
  • I don't see how the input request is used in each service class, the httpRequest input param of the service class constructor is never used inside the generated API methods; they continue to use the singleton reference client.
/**
 * Generated sdk.gen.ts
 */

export class MySDK {

  public readonly customers: Customers;
  
  public readonly request: BaseHttpRequest;
  
  constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = ) {
  this.request = new HttpRequest({
	  BASE: config?.BASE ?? 'https://sandbox.dev.clover.com',
	  VERSION: config?.VERSION ?? '3.0',
	  WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
	  CREDENTIALS: config?.CREDENTIALS ?? 'include',
	  TOKEN: config?.TOKEN,
	  USERNAME: config?.USERNAME,
	  PASSWORD: config?.PASSWORD,
	  HEADERS: config?.HEADERS,
	  ENCODE_PATH: config?.ENCODE_PATH,
	  interceptors: {
		  request: config?.interceptors?.request ?? new Interceptors(),
		  response: config?.interceptors?.response ?? new Interceptors(),
      },
  });
  
  this.customers = new Customers(this.request);
  }
}

/**
 * Generated service.gen.ts
 */

//... imports here

export const client = createClient(createConfig());

export class Customers {
  constructor(public readonly httpRequest: BaseHttpRequest) {}

  /**
   * Get a list of customers
   * Gives information for every customer of a merchant by default.
   */
  public getCustomers<ThrowOnError extends boolean = false>(
    options: Options<CustomersGetCustomersData, ThrowOnError>,
  ) {
    return (options?.client ?? client).get<
      CustomersGetCustomersResponse,
      CustomersGetCustomersError,
      ThrowOnError
    >({
      ...options,
      url: '/v3/merchants/{mId}/customers',
    });
  }
}

@mrlubos
Copy link
Member

mrlubos commented Aug 14, 2024

@bombillazo your config settings would be helpful here

@bombillazo
Copy link

Im using the js API to autogen programmatically, here it is:

await createClient({
  client: '@hey-api/client-fetch',
  input: './service/myapi/sdk/myapi.openapi.json',
  name: 'MySDK',
  output: {
    path: './service/myapi/client',
  },
  services: {
    asClass: true,
    methodNameBuilder: (operation) => {
      return camelCase(operation.id?.split('.').pop() || operation.name);
    },
    name: '{{name}}',
  },
});

@Nick-Lucas
Copy link

Can you try one of the older clients like Axios or Fetch?

It's not preferred going forward but given the named client hasn't been given much attention lately it wouldn't be a surprise to see it's incompatible with the decoupled client

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

Also, wanted to say I appreciate the help and engagement on this!

So, using classic Fetch definitely fixed most of those observations! As you said, the new decoupled @hey-api/client-fetch client hasn't been properly integrated with the deprecated name option.

I tried using the @hey-api/client-fetch client with bundle = true but didn't work. It doesnt fill the gap like the legacy fetch did. Ideally id love to use the new client.

Now I see it all connect together:

/**
 * Generated sdk.gen.ts
 */

export class MySDK {
  public readonly customers: Customers;

  public readonly request: BaseHttpRequest;

  constructor(
    config?: Partial<OpenAPIConfig>,
    HttpRequest: HttpRequestConstructor = FetchHttpRequest
  ) {
    this.request = new HttpRequest({
      BASE: config?.BASE ?? "https://sandbox.dev.clover.com",
      VERSION: config?.VERSION ?? "3.0",
      WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
      CREDENTIALS: config?.CREDENTIALS ?? "include",
      TOKEN: config?.TOKEN,
      USERNAME: config?.USERNAME,
      PASSWORD: config?.PASSWORD,
      HEADERS: config?.HEADERS,
      ENCODE_PATH: config?.ENCODE_PATH,
      interceptors: {
        request: config?.interceptors?.request ?? new Interceptors(),
        response: config?.interceptors?.response ?? new Interceptors(),
      },
    });

    this.customers = new Customers(this.request);
  }
}

/**
 * Generated service.gen.ts
 */

//... imports here

export class Customers {
  constructor(public readonly httpRequest: BaseHttpRequest) {}

  /**
   * Get a list of customers
   * Gives information for every customer of a merchant by default.
   * @param data The data for the request.
   * @param data.mId Merchant Id
   * @param data.filter Filter fields: [customerSince, firstName, lastName, emailAddress, phoneNumber, marketingAllowed, fullName, id, deletedTime]
   * @param data.expand Expandable fields: [addresses, emailAddresses, phoneNumbers, cards, metadata]
   * @returns unknown
   * @throws ApiError
   */
  public getCustomers(
    data: CustomersGetCustomersData
  ): CancelablePromise<CustomersGetCustomersResponse> {
    return this.httpRequest.request({
      method: "GET",
      url: "/v3/merchants/{mId}/customers",
      path: {
        mId: data.mId,
      },
      query: {
        filter: data.filter,
        expand: data.expand,
      },
    });
  }
}

@bombillazo
Copy link

bombillazo commented Aug 14, 2024

In conclusion, @hey-api/client-fetch and the deprecated name option are not compatible at the moment. But classic fetch with name produces exactly what my use cases needs (as of this discussion, I'll still validate that when I do some actual code testing).

I think a good approach would be to rename name to something more meaningful to specify it is to generate instantiable clients, and then refactor the instantiable generation to support the new hey-api fetch

@volkandkaya
Copy link

Seems like core folder is not generated if you use @hey-api/client-fetch & name together.

client: 'legacy/axios', gives a Typescript error but generates the core folder.

@StephenHodgson
Copy link

Seems like core folder is not generated if you use @hey-api/client-fetch & name together.

client: 'legacy/axios', gives a Typescript error but generates the core folder.

Running into the same problem. What is the best path to create bundled client? I need to add some custom authorization for signed jwts for each endpoint and having a main client entry point seems like the best approach here.

I tried a few of the suggestions in this thread but none worked out for me so far for my use case.

@StephenHodgson
Copy link

StephenHodgson commented Oct 12, 2024

fwiw, I added a very basic wrapper api client that just forwarded my services to the client class.

openapi-ts.config.ts:

import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
    client: {
        bundle: true,
        name: '@hey-api/client-fetch'
    },
    input: './app_store_connect_api_openapi.json',
    output: './src/app_store_connect_api',
    services: {
        asClass: false,
    }
});

client.ts:

import * as services from './app_store_connect_api/services.gen';
import { generateAuthToken } from "./auth";
import fs from 'fs';

/**
 * Options for configuring the App Store Connect client.
 * @see https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api
 */
interface AppStoreConnectOptions {
  /**
   * The issuer ID associated with the private key.
   */
  issuerId?: string;
  /**
   * The ID of the private key.
   */
  privateKeyId?: string;
  /**
   * The private key in PEM format.
   */
  privateKey?: string;
  /**
   * The path to the private key file.
   */
  privateKeyFile?: string;
  /**
   * A bearer token can be provided directly, which will be used instead of generating a new token
   */
  bearerToken?: string;
  /**
   * The time (in seconds) until the token expires (default 10 minutes)
   */
  expirationTime?: number;
}

export default class AppStoreConnectClient {
  private appStoreConnectOptions: AppStoreConnectOptions;
  private bearerToken: string | null = null;
  private bearerTokenGeneratedAt = 0;
  api = services;

  constructor(appStoreConnectOptions: AppStoreConnectOptions) {
    if (!appStoreConnectOptions) {
      throw new Error('AppStoreConnectOptions is required');
    }
    this.appStoreConnectOptions = appStoreConnectOptions;
    services.client.setConfig({ baseUrl: 'https://api.appstoreconnect.apple.com' });
    services.client.interceptors.request.use(async (request, _options): Promise<Request> => {
      request.headers.set('Authorization', `Bearer ${await this.getToken()}`);
      return request;
    });
  }

  private async getToken() {
    const defaultExpirationTime = 600; // 10 minutes
    if (this.appStoreConnectOptions.bearerToken) {
      this.bearerToken = this.appStoreConnectOptions.bearerToken;
    } else {
      if (this.appStoreConnectOptions.privateKeyId &&
        this.appStoreConnectOptions.issuerId &&
        (this.appStoreConnectOptions.privateKey || this.appStoreConnectOptions.privateKeyFile)) {
        if (!this.bearerToken || this.bearerTokenGeneratedAt + (this.appStoreConnectOptions.expirationTime ?? defaultExpirationTime) * 1000 < Date.now()) {
          if (this.appStoreConnectOptions.privateKeyFile) {
            this.appStoreConnectOptions.privateKey = await fs.promises.readFile(this.appStoreConnectOptions.privateKeyFile, 'utf8');
          }
          this.bearerToken = await generateAuthToken({
            privateKeyId: this.appStoreConnectOptions.privateKeyId,
            issuerId: this.appStoreConnectOptions.issuerId,
            privateKey: this.appStoreConnectOptions.privateKey!,
            expirationTime: this.appStoreConnectOptions.expirationTime,
          });
        }
      } else {
        throw new Error('Bearer token or private key information is required to generate a token');
      }
    }

    return this.bearerToken;
  }
}

export { AppStoreConnectOptions, AppStoreConnectClient };

sdk caller:

async function getLatestAppStoreBuildNumber(bundleId: string): Promise<number> {
    const { data: response, error } = await appStoreConnectClient.api.buildsGetCollection({
        query: {
            'fields[apps]': ['bundleId'],
            'filter[app]': [bundleId],
            sort: ['-version'],
            limit: 1,
        }
    });
    if (error) {
        throw new Error(`Error fetching builds: ${JSON.stringify(error)}`);
    }
    if (!response) {
        return 0;
    }
    if (response.data.length === 0) {
        return 0;
    }
    return Number(response.data[0].attributes.version);
}

notes:

while overall easy to generate api client, it isn't very easy to interact with it.

for example, when calling the service endpoints, the optional query parameters and data aren't really formatted for simple function calls. Seems a services binding layer is still required if you want sdk consumers to interact with it with ease.

@greenking19
Copy link

I've been reading for an hour and don't understand your needs. Can I ask you some questions?
I am using openapi-ts in the Vue project

createClient() // client1
createClient() //. client2

Then when calling the method of sdk.gen.ts

apiGetUser({
  client: client1
})

I want to understand your needs to avoid not knowing how to solve such problems in the future
🫣

@mrlubos
Copy link
Member

mrlubos commented Dec 3, 2024

I think you should be good @greenking19, this thread is around functionality that's not natively supported by this package. If what you have works for you, there should be no need for a custom client

@Stono
Copy link
Contributor

Stono commented Dec 10, 2024

I think a good approach would be to rename name to something more meaningful to specify it is to generate instantiable clients, and then refactor the instantiable generation to support the new hey-api fetch

This is a good suggestion I think. The single named client is the only reason I can't switch away from using "name" and legacy client to the newer one.

@ScottGuymer
Copy link

ScottGuymer commented Jan 22, 2025

After experimenting a little this is also something I am missing in the capabilities.

I have a scenario where there are multiple instances of the same API at different URLs that I might need to interact with at the same time.

Therefore, it would be great to just new up an instance of the API for each and interact with them independently and not worry about implicit client settings or having to pass a client into each call.

Something like this would be great, fully encapsulate the client and its settings inside this instance of the class representing the API.

const apiInstance1 = new MyGeneratedApi({url: 'my/new/url'});
const apiInstance2 = new MyGeneratedApi({url: 'my/new/url'});

apiInstance1.search(...)
apiInstance2.search(...)

Currently, as a consumer, you have to know far too much about the implementation than should be needed.

For example

import { client, search } from 'my-api';

// set intsance one URL
client.setConfig(...)
// query instance one
search(...)

// set instance two URL
client.setConfig(...)
// query instance two
search(...)


// or
import { createClient } from '@hey-api/client-fetch';

const clientInstanceOne = createClient({
  ...
});

const clientInstanceTwo = createClient({
  ...
});


search({..., client: clientInstanceOne});
search({..., client: clientInstanceTwo});

Neither of the above are great...

--

@douglasg14b FWIW your workaround is nice and works but should just be the way it works IMO.

@mrlubos
Copy link
Member

mrlubos commented Mar 31, 2025

Hey hey, let's move this discussion to the other issue, I've posted an update there. Custom clients in progress! #1213 (comment)

@mrlubos mrlubos closed this as completed Mar 31, 2025
ibash added a commit to ibash/openapi-ts that referenced this issue Apr 9, 2025
This adds an option asInstance that makes the sdk class instantiable.
Specifically:

1. Methods are instance methods instead of static
2. The sdk takes a client as a parameter in the constructor, and sdk
   methods use the instance of the client.

This allows creating multiple instances of the sdk that work
independently. My use case is using a client during server side
rendering where each sdk instance is authenticated with the end-user's
auth token which has limited permissions (as opposed to using a single
client with admin permissions).

Contributes hey-api#671
@ibash
Copy link

ibash commented Apr 9, 2025

I opened a PR that allows instantiating an sdk with a client. So you can have multiple instances:

export class DefaultService {
    constructor(public client: Client) { }
    
    /**
     * Foo
     */
    public foo<ThrowOnError extends boolean = false>(options: Options<FooData, ThrowOnError>) {
        return (options.client ?? this.client).post<FooResponse, FooError, ThrowOnError>({
            url: '/foo',
            ...options,
            headers: {
                'Content-Type': 'application/json',
                ...options?.headers
            }
        });
    }
 ...

#1932

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature 🚀 New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.