Skip to content

feat: implement TypeScript Connect API client#3673

Open
zackverham wants to merge 8 commits intomainfrom
ts-connect-client
Open

feat: implement TypeScript Connect API client#3673
zackverham wants to merge 8 commits intomainfrom
ts-connect-client

Conversation

@zackverham
Copy link
Collaborator

@zackverham zackverham commented Mar 9, 2026

Intent

Implement a TypeScript Connect API client package (@posit-dev/connect-api) and validate it against contract tests alongside the existing Go backend.

Type of Change

  • New Feature
  • Refactoring

Approach

Created a standalone packages/connect-api/ package with a ConnectAPI class that wraps the Posit Connect REST API. All 15 API methods are implemented:

  • Auth & User: TestAuthentication, GetCurrentUser
  • Content CRUD: ContentDetails, CreateDeployment, UpdateDeployment
  • Environment: GetEnvVars, SetEnvVars
  • Bundles: UploadBundle, LatestBundleID, DownloadBundle
  • Deployment: DeployBundle, WaitForTask, ValidateDeployment
  • Server Info: GetIntegrations, GetSettings

Key design decisions:

  • axios for HTTP — uses axios.create() with a pre-configured baseURL, default Authorization header, and validateStatus: () => true so the client controls error handling rather than axios auto-throwing on non-2xx
  • Raw API responses — every method returns the exact JSON the Connect API sends back (full DTOs), rather than constructing new objects or extracting single fields. The contract test adapter handles any field extraction needed.
  • Branded ID types (ContentID, BundleID, TaskID) for type safety on parameters

User Impact

None — this is preparatory code.

Automated Tests

  • 52 unit tests in packages/connect-api/ (vitest) covering all methods, error paths, and URL construction
  • All 68 contract tests pass with both backends (npx vitest run and API_BACKEND=typescript npx vitest run)
  • CI runs contract tests against both Go and TypeScript backends

🤖 Generated with Claude Code

zackverham and others added 4 commits March 9, 2026 15:35
Replace the stub TypeScriptDirectClient with a full implementation that
makes HTTP requests directly to the Connect server, mirroring the Go
client's behavior. All 15 API methods are implemented and pass the
same 68 contract tests that validate the Go client.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move the TypeScript Connect API client from the contract test file into
a standalone @posit-dev/connect-client package with proper types, error
classes, and barrel exports. The contract test adapter becomes a thin
wrapper that imports from the new package. All 68 contract tests pass
with both Go and TypeScript backends.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a matrix strategy to the contract-tests job so it runs once with
API_BACKEND=go (existing behavior) and once with API_BACKEND=typescript
(new ConnectClient from @posit-dev/connect-client). The TypeScript
backend skips Go setup since it doesn't need the harness binary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +12 to +14
"dependencies": {
"@posit-dev/connect-client": "file:../../packages/connect-client"
},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we continue down this path (which I think we should) we should consider doing a few things:

I think its a good idea to have separate packages. It makes the way we import and deal with some of the code around the homeView much easier to deal with. Right now it imports types from extension/vscode by importing with ../../.. which is not great.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotNomad would you say creating a shared configuration package should be part of this PR, or part of follow-up work?

// HTTP helpers
// ---------------------------------------------------------------------------

private async request(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep using axios despite some of the problems we've had so we don't have to write our own error handling logic, or change how we do it in a lot of spots. That would simplify what we have here quite a bit.

Comment on lines +145 to +151
return {
id: dto.guid,
username: dto.username,
first_name: dto.first_name,
last_name: dto.last_name,
email: dto.email,
};
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a preference in design, but in my opinion it has a lot of benefits:

I would prefer to return the exact thing the API call responds with, including all data, and not creating new objects.

This will keep this code extremely single use and the only thing we will need to adjust overtime are the types. We won't have to adjust the functions since they just pass the response right through. We can always extract the data from the response.

It is much more flexible.

You can read more in our README.md for the Go API client we wrote: https://github.com/posit-dev/publisher/blob/183abd7d744fb8f6d8bcffc323717547521eca57/extensions/vscode/src/api/README.md

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see this is mapping a UserDTO to a User, but I think we should avoid this. It would make the initial migration easier, but once it is complete it will be much more difficult to understand why the attributes are different.

Down to discuss temporary trade-offs.

Comment on lines +7 to +11
export type ContentID = string & { readonly __brand: "ContentID" };
export type BundleID = string & { readonly __brand: "BundleID" };
export type TaskID = string & { readonly __brand: "TaskID" };
export type UserID = string & { readonly __brand: "UserID" };
export type GUID = string & { readonly __brand: "GUID" };
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Branded Types are a nice addition to avoid ID overlap 👍

@zackverham zackverham changed the title feat: implement TypeScript Connect API client for contract tests feat: implement TypeScript Connect API client Mar 10, 2026
zackverham and others added 4 commits March 10, 2026 08:51
Rename the package from @posit-dev/connect-client to @posit-dev/connect-api
and add comprehensive unit tests for the ConnectClient class and error types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Also renames ConnectClientOptions to ConnectAPIOptions and
ConnectClientError to ConnectAPIError for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace native fetch with axios for HTTP requests in the ConnectAPI
class. Uses axios.create() with baseURL, default Authorization header,
and validateStatus: () => true to preserve existing error handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stop constructing new objects in client methods — return the exact
response data from the Connect API instead. Removes the User type
(a client-side invention) and updates the contract test adapter to
extract fields from full DTOs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
};
}

private async dispatch(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we get rid of the go API client, we can clean up the shape of this stub, or just use the ConnectAPI client directly in test.

Right now the test shape is coupled to the go client's shape so we can have an apples-to-apples comparison in the contract tests, but we won't always need to have the apples-to-apples comparison we're running here.

});

// ---------------------------------------------------------------------------
// getSettings
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be convinced that the right approach will end up being splitting these out into atomic calls - maybe something we can feel out as we go along.

* Validates credentials and checks user state (locked, confirmed, role).
* Returns the full UserDTO on success; throws AuthenticationError otherwise.
*/
async testAuthentication(): Promise<UserDTO> {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not strictly a light wrapper around an API call, but this does seem pretty useful to have on the API client.


/**
* Fetches composite server settings from 7 separate endpoints,
* mirroring the Go client's GetSettings behavior.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging again - not sure if we want to mimic this or not

// Integration type
// ---------------------------------------------------------------------------

export interface Integration {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dotNomad is there any convention around calling something vs DTO?

@zackverham zackverham marked this pull request as ready for review March 10, 2026 18:19
@zackverham zackverham requested a review from a team as a code owner March 10, 2026 18:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants