Skip to content

Implement ngx-openapi-resource library - #1

Merged
luchsamapparat merged 15 commits into
mainfrom
copilot/implement-openapi-resource-library
Jul 17, 2026
Merged

luchsamapparat merged 15 commits into
mainfrom
copilot/implement-openapi-resource-library

Conversation

Copilot AI commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Implements the ngx-openapi-resource Angular library — type-safe wrappers around Angular's httpResource and HttpClient driven by types generated by openapi-typescript. Wrong paths, misspelled parameter names, and mismatched body shapes are caught at compile time.

Core API

createOpenApiResource<Paths>(config) — reactive factory

Wraps Angular's httpResource; re-fetches automatically when signals change. Return undefined from the request function to idle the resource.

const api = createOpenApiResource<paths>({ baseUrl: 'https://api.example.com' });

// Reactive GET — refetches when petId() changes
const pet = api.get('/pets/{petId}', () => ({ params: { path: { petId: petId() } } }));

// POST with typed body
const result = api.post('/pets', () => ({ body: { name: 'Buddy' } }));

OpenApiHttpClient<Paths> — imperative base class

Wraps HttpClient with typed get/post/put/patch/delete methods. Extend per-API:

@Injectable({ providedIn: 'root' })
export class PetApiClient extends OpenApiHttpClient<paths> {
  constructor() { super({ baseUrl: 'https://api.example.com' }); }
}

// Usage
client.get('/pets/{petId}', { params: { path: { petId: 42 } } })  // → Observable<Pet>
client.post('/pets', { body: { name: 'Buddy' } })                  // → Observable<Pet>

Key implementation notes

  • PathRecord = {} — matches the same loose constraint used by openapi-typescript-helpers, avoiding TS2344 errors when passing generated paths interfaces
  • OpOf<Paths, Path, Method> — conditional type inference (infer Op) used instead of direct Paths[Path][Method] indexing, which fails when Paths extends {}
  • OperationParamsObject<Op> — inner { path?, query? } type; required path params propagate through ParamEntry<'params', ...> so omitting them is a compile error
  • OpenApiOperationRequest<Op> uses { params: { path?, query? }, body? } — matching the openapi-fetch convention exactly
  • buildUrl URI-encodes path parameter substitutions; toHttpParams handles scalar, array, null, and undefined query values
  • OpenApiHttpClient returns responseType: 'json', observe: 'body' to steer TypeScript to the correct HttpClient generic overload
  • buildUrl/toHttpParams are extracted into a side-effect-free url-utils.ts module shared by both open-api-resource and open-api-http-client, avoiding unnecessary bundle coupling

Files

File Purpose
src/lib/types.ts All TypeScript utility types
src/lib/url-utils.ts Shared buildUrl and toHttpParams helpers
src/lib/open-api-resource.ts createOpenApiResource factory
src/lib/open-api-http-client.ts OpenApiHttpClient base class
src/public-api.ts Public API surface
src/lib/ngx-openapi-resource.spec.ts 21 unit + integration tests
projects/demo/openapi.yaml OpenAPI 3.0 spec for the demo (JSONPlaceholder todos API, including mutations)
projects/demo/src/app/api.types.ts Types generated from openapi.yaml via openapi-typescript
projects/demo/src/app/app.ts Demo using both APIs against jsonplaceholder (GET, POST, PUT, DELETE)

Demo

The demo uses a real OpenAPI 3.0 YAML spec (projects/demo/openapi.yaml) describing the JSONPlaceholder todos API, including full CRUD endpoints (GET /todos, POST /todos, GET /todos/{id}, PUT /todos/{id}, DELETE /todos/{id}). Types are generated from it using openapi-typescript and committed at projects/demo/src/app/api.types.ts. To regenerate:

npm run generate:demo

The demo app demonstrates:

  • Reactive GET — createOpenApiResource fetching a paginated list with query params
  • Reactive POST — createOpenApiResource with a signal-driven form; reloads the list on success via todos.reload()
  • Imperative GET / PUT / DELETE — OpenApiHttpClient with typed path params and request bodies
Original prompt

Start implementation

The user has attached the following file paths as relevant context:

  • .github\copilot-instructions.md
  • AGENTS.md
[Chronological Review: The conversation began with inquiries about the `openapi-typescript` npm package and Angular's new Resource Signals, particularly the `httpResource`. The user then expressed interest in creating a variant of `httpResource` that integrates with types generated by `openapi-typescript`. Following this, the user requested a plan to build a library named `openApiResource`, which would utilize Angular's `HttpClient` and support modern best practices. The user also suggested removing unnecessary components from the workspace and ensuring the library is built for publication. The conversation progressed with discussions about the potential need for an `OpenApiHttpClient` and the installation of relevant packages. The user agreed with the recommendations and finally requested to start the implementation.]

[Intent Mapping:

  1. "How does the openapi-typescript npm package work?" - Understanding the package functionality.
  2. "Explain Angular's new Resource Signals to me and especially the new httpResource" - Seeking knowledge about Angular features.
  3. "Is it possible to build a variant of httpResource that works with the types generated by openapi-typescript similar to how openapi-fetch works?" - Exploring integration possibilities.
  4. "Make a plan for creating a library that provides an openApiResource..." - Requesting a structured approach to library development.
  5. "You can remove everything from the workspace that is not necessary for our goal of building a library" - Streamlining the workspace.
  6. "Make sure the library is built according to modern best practices for Angular libraries." - Ensuring quality and compliance with standards.
  7. "If httpResource only supports GET requests, maybe the library should also provide an OpenApiHttpClient..." - Suggesting additional functionality.
  8. "You can install openapi-typescript, openapi-typescript-helpers and other packages that might help" - Directing package installations.
  9. "What is the difference between those options and how would they affect the implementation?" - Seeking clarification on implementation choices.
  10. "I agree with your recommendations" - Confirming the proposed plan.
  11. "Start implementation" - Initiating the development process.]

[Technical Inventory:

  • openapi-typescript: A package for generating TypeScript types from OpenAPI specifications.
  • Angular's HttpClient: A service for making HTTP requests in Angular applications.
  • openapi-fetch: A utility for fetching data using OpenAPI types.
  • openapi-typescript-helpers: A package that may assist in working with OpenAPI-generated types.]

[Code Archaeology: No specific files or code changes have been documented yet, as the conversation is still in the planning and initiation phase.]

[Progress Assessment:

  • Completed Tasks: User has confirmed the plan for the library and agreed with recommendations.
  • Partially Complete Work: The implementation phase has just been initiated, with no code changes yet documented.]

[Context Validation: All necessary context for continuing the library development is captured, including user goals, technical requirements, and the need for best practices.]

[Recent Commands Analysis:

  • Last Agent Commands: "Start implementation" - This command initiated the development process for the openApiResource library.
  • Tool Results Summary: No specific tool results were returned as the implementation phase has just begun.
  • Pre-Summary State: The agent was preparing to start the implementation of the library when the token budget was exceeded.
  • Operation Context: The command to start implementation aligns with the user's goal of creating a functional library that integrates with OpenAPI types and Angular's HttpClient.]
1. Conversation Overview: - Primary Objectives: The user aims to create a library named `openApiResource` that integrates with Angular's `HttpClient` and works with types generated by `openapi-typescript`. - Session Context: The conversation has progressed from understanding existing technologies to planning and initiating the implementation of a new library. - User Intent Evolution: The user has moved from seeking information to actively planning and initiating the development of a library.
  1. Technical Foundation:
  • openapi-typescript: A package for generating TypeScript types from OpenAPI specifications.
  • Angular's HttpClient: A service for making HTTP requests in Angular applications.
  • openapi-fetch: A utility for fetching data using OpenAPI types.
  • openapi-typescript-helpers: A package that may assist in working with OpenAPI-generated types.
  1. Codebase Status:
  • No specific files or code changes have been documented yet, as the conversation is still in the planning and initiation phase.
  1. Problem Resolution:...

Created from VS Code.


💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Copilot AI and others added 2 commits March 26, 2026 22:46
Agent-Logs-Url: https://github.com/holisticon/ngx-openapi-resource/sessions/7e3b4107-a070-45c1-9b7d-0b22cc27166e

Co-authored-by: luchsamapparat <875017+luchsamapparat@users.noreply.github.com>
… OpenApiHttpClient

Agent-Logs-Url: https://github.com/holisticon/ngx-openapi-resource/sessions/7e3b4107-a070-45c1-9b7d-0b22cc27166e

Co-authored-by: luchsamapparat <875017+luchsamapparat@users.noreply.github.com>
Copilot AI changed the title [WIP] Start implementation of openApiResource library Implement ngx-openapi-resource library Mar 26, 2026
Copilot AI requested a review from luchsamapparat March 26, 2026 23:06
@luchsamapparat
luchsamapparat requested review from Copilot and removed request for luchsamapparat March 27, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR implements the new ngx-openapi-resource Angular library, providing type-safe wrappers around Angular’s httpResource (reactive) and HttpClient (imperative) driven by openapi-typescript-generated paths types.

Changes:

  • Added createOpenApiResource<Paths>() for typed, signal-driven HTTP resources (GET/POST/PUT/PATCH/DELETE).
  • Added OpenApiHttpClient<Paths> base class for typed imperative HTTP calls.
  • Introduced shared OpenAPI utility types plus comprehensive unit/integration tests and updated demo/docs.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
projects/ngx-openapi-resource/src/public-api.ts Replaced default Angular CLI export with explicit library exports/types.
projects/ngx-openapi-resource/src/lib/types.ts Added OpenAPI utility types for parameters/body/options typing.
projects/ngx-openapi-resource/src/lib/open-api-resource.ts Implemented createOpenApiResource plus URL/query helpers.
projects/ngx-openapi-resource/src/lib/open-api-http-client.ts Implemented typed HttpClient wrapper base class.
projects/ngx-openapi-resource/src/lib/ngx-openapi-resource.ts Removed the starter placeholder component.
projects/ngx-openapi-resource/src/lib/ngx-openapi-resource.spec.ts Added unit tests for helpers + integration tests for both APIs.
projects/ngx-openapi-resource/package.json Added openapi-typescript-helpers as a peer dependency.
projects/ngx-openapi-resource/README.md Documented installation and usage of both APIs.
projects/demo/src/app/app.ts Updated demo to showcase reactive + imperative usage.
projects/demo/src/app/app.config.ts Added provideHttpClient() to support demo HTTP calls.
package.json Added OpenAPI tooling dependencies to the workspace.
package-lock.json Lockfile updated for new dependencies and workspace naming.
README.md Updated root README with library usage docs.

Comment on lines +118 to +122
url: buildUrl(baseUrl, String(path), r.path),
params: toHttpParams(r.query),
headers: r.headers as Record<string, string>,
context: r.context as import('@angular/common/http').HttpContext,
withCredentials: r.withCredentials,

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

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

In createOpenApiResource, headers is cast to Record<string, string> before passing to httpResource. This is narrower than the library's own OpenApiRequestOptions.headers (which allows HttpHeaders and multi-valued headers) and can hide type errors for callers. Prefer passing through the original headers type (or casting to HttpHeaders | Record<string, string | ReadonlyArray<string>>) consistently with OpenApiRequestOptions.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in commit 94ba395.

Changes made:

  • toRequestShape now properly types headers as HttpHeaders | Record<string, string | ReadonlyArray<string>> (matching HttpResourceRequest.headers) — no cast needed anymore
  • All 5 as Record<string, string> casts in createOpenApiResource removed
  • buildUrl/toHttpParams extracted to a new url-utils.ts module — open-api-http-client now imports from there instead of open-api-resource, removing the bundle coupling
  • OpenApiHttpClient.delete() signature updated to accept OpenApiOperationRequest (with body) for parity with createOpenApiResource.delete()
  • openapi-typescript moved to devDependencies

Comment thread package.json
Comment on lines 19 to 22
"@angular/router": "^21.2.0",
"openapi-typescript": "^7.13.0",
"openapi-typescript-helpers": "^0.1.0",
"rxjs": "~7.8.0",

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

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

openapi-typescript is a CLI/codegen tool and is not imported/used at runtime in the workspace (only referenced in docs). Keeping it under dependencies will install it for production builds and increase install footprint; it should typically be a devDependency instead.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot should it be a peer dependency?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

openapi-typescript should stay as a devDependency in the workspace root, not a peer dependency.

The library itself never imports from openapi-typescript at runtime or compile time — only from openapi-typescript-helpers (which is already a peer dependency in the library's package.json). openapi-typescript is a CLI tool consumers run once during their own build to generate paths types from an OpenAPI spec. Since it doesn't need to be present in the consumer's node_modules at runtime (or even at Angular compile time), listing it as a peer dependency would impose an unnecessary installation requirement on every consumer of this library.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already applied in commit a2e9119 — openapi-typescript was moved from dependencies to devDependencies in the workspace root package.json. The library's own package.json does not list it at all (neither as dependency nor peer dependency), since the library only imports from openapi-typescript-helpers at compile time.

Comment thread projects/ngx-openapi-resource/src/lib/open-api-http-client.ts Outdated
Comment on lines +13 to +16
PathRecord,
} from './types';
import { buildUrl, toHttpParams } from './open-api-resource';

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

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

OpenApiHttpClient imports buildUrl/toHttpParams from open-api-resource.ts, which also imports httpResource types/logic. This couples the imperative client to the resource module and can force consumers using only OpenApiHttpClient to still pull in the httpResource implementation at bundle time. Consider moving shared helpers into a small side-effect-free util module (e.g. url.ts/http-params.ts) that both files import from.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot apply changes based on this feedback

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Already applied in commit 94ba395. buildUrl and toHttpParams were extracted to a new src/lib/url-utils.ts module. open-api-http-client.ts now imports directly from ./url-utils instead of ./open-api-resource, removing the bundle coupling.

luchsamapparat and others added 3 commits March 27, 2026 08:05
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…pescript to devDependencies

Agent-Logs-Url: https://github.com/holisticon/ngx-openapi-resource/sessions/8914c92c-31ca-4466-a099-883c15843f42

Co-authored-by: luchsamapparat <875017+luchsamapparat@users.noreply.github.com>
…ce-library'

Co-authored-by: luchsamapparat <875017+luchsamapparat@users.noreply.github.com>
@luchsamapparat

Copy link
Copy Markdown
Member

@copilot for the demo, use an OpenAPI YAML spec and generate types from it using openapi-typescript.

Copilot AI commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot for the demo, use an OpenAPI YAML spec and generate types from it using openapi-typescript.

Done in commit 37c6631.

  • Added projects/demo/openapi.yaml — an OpenAPI 3.0 spec describing the JSONPlaceholder todos API (GET /todos with _limit query param, GET /todos/{id})
  • Added a generate:demo script to package.json: openapi-typescript projects/demo/openapi.yaml -o projects/demo/src/app/api.types.ts
  • Committed the generated projects/demo/src/app/api.types.ts
  • Updated app.ts to import paths and components from ./api.types — the inline type definitions are gone

@luchsamapparat

Copy link
Copy Markdown
Member

@copilot implement this plan

Mitigation Plan: Fix Deviations from Original Design

Issues to fix

  1. Params shape — flat { path, query, body } instead of agreed openapi-fetch-style { params: { path, query }, body }
  2. Demo mutations — no POST/PUT/DELETE shown in demo
  3. Demo YAML spec — read-only todos API, should have mutation endpoints
  4. openapi-typescript-helpers in workspace dependencies — should be devDependencies

Fix 1: Params shape (breaking — affects types, resource, http-client, tests, demo)

The agreed shape was { params: { path?, query? }, body? } matching openapi-fetch exactly.

types.ts

  • Rename OperationParameters<Op> → OperationParamsObject<Op> (the inner { path?, query? } object)
  • Change OpenApiOperationRequest<Op> to wrap path/query under a params key:
    { params?: OperationParamsObject<Op> } & OperationBody<Op> & OpenApiRequestOptions

open-api-resource.ts

  • toRequestShape() must read req.params?.path and req.params?.query instead of req.path / req.query
  • Update all 5 methods accordingly
  • Update JSDoc examples

open-api-http-client.ts

  • toShape() must read opts.params?.path, opts.params?.query
  • Update all 5 methods accordingly
  • Update JSDoc examples

ngx-openapi-resource.spec.ts

Update all test call sites:

  • { query: { limit: 5 } } → { params: { query: { limit: 5 } } }
  • { path: { petId: 42 } } → { params: { path: { petId: 42 } } }
  • { path: { petId: 1 }, body: ... } → { params: { path: { petId: 1 } }, body: ... }

app.ts (demo)

  • api.get('/todos', () => ({ query: { _limit: 5 } })) → api.get('/todos', () => ({ params: { query: { _limit: 5 } } }))
  • client.get('/todos/{id}', { path: { id: 1 } }) → client.get('/todos/{id}', { params: { path: { id: 1 } } })

Fix 2 & 3: Demo spec and mutations

projects/demo/openapi.yaml

Extend with mutation endpoints and a NewTodo schema:

  • GET /todos (query: { _limit?: number }) → Todo[]
  • POST /todos (body: NewTodo) → Todo
  • GET /todos/{id} → Todo
  • PUT /todos/{id} (body: NewTodo) → Todo
  • DELETE /todos/{id} → 204
  • Add NewTodo schema: { title: string, completed?: boolean, userId: number }

projects/demo/src/app/api.types.ts

Regenerate via npm run generate:demo and commit.

projects/demo/src/app/app.ts

Add a mutation section demonstrating:

  • A text input + button calling api.post('/todos', () => ({ body: { ... } })) via createOpenApiResource
  • Calls todos.reload() after submit to refresh the list
  • Imperative PUT /todos/{id} via OpenApiHttpClient
  • Signals for form state (titleInput, submitting)

Fix 4: openapi-typescript-helpers in workspace deps

package.json (root)

  • Move openapi-typescript-helpers from dependencies to devDependencies
  • It is only needed at compile time; the published library already declares it as a peer dep

Files to change

File Change
projects/ngx-openapi-resource/src/lib/types.ts Reshape OpenApiOperationRequest — nest path/query under params
projects/ngx-openapi-resource/src/lib/open-api-resource.ts Read r.params?.path, r.params?.query; update JSDoc
projects/ngx-openapi-resource/src/lib/open-api-http-client.ts Read opts.params?.path, opts.params?.query; update JSDoc
projects/ngx-openapi-resource/src/lib/ngx-openapi-resource.spec.ts Update all call sites to nested params
projects/demo/openapi.yaml Add POST/PUT/DELETE endpoints + NewTodo schema
projects/demo/src/app/api.types.ts Regenerate + commit
projects/demo/src/app/app.ts Add mutation demo section
package.json (root) Move openapi-typescript-helpers to devDependencies

Verification

  1. ng build ngx-openapi-resource — zero TS errors
  2. ng test ngx-openapi-resource — all specs pass
  3. ng serve demo — mutations section renders and works
  4. Hover: api.get('/todos/{id}', () => ({ params: { path: { id: 1 } } })) — infers Todo
  5. Hover: client.post('/todos', { body: { ... } }) — body typed as NewTodo, return is Observable<Todo>
  6. Omitting required params.path when an id path param is required → TypeScript compile error

…ith mutations, move openapi-typescript-helpers to devDeps

Agent-Logs-Url: https://github.com/holisticon/ngx-openapi-resource/sessions/fde12950-defb-4db6-8c3f-1fdbb5fc1422

Co-authored-by: luchsamapparat <875017+luchsamapparat@users.noreply.github.com>

Copilot AI commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot implement this plan

Mitigation Plan: Fix Deviations from Original Design

...

Implemented in commit 6c6e076. All four fixes applied:

  1. Params shape — OperationParameters renamed to OperationParamsObject; OpenApiOperationRequest now uses { params: { path?, query? }, body? } via ParamEntry<'params', OperationParamsObject<Op>>. Path params still enforce required-ness at compile time. All call sites in open-api-resource.ts, open-api-http-client.ts, tests, and demo updated.

  2. Demo mutations — Added reactive POST (via createOpenApiResource) with a form that reloads the list on success, plus imperative PUT and DELETE via OpenApiHttpClient.

  3. Demo YAML — Extended with POST /todos, PUT /todos/{id}, DELETE /todos/{id}, and NewTodo schema. api.types.ts regenerated.

  4. openapi-typescript-helpers — Moved from dependencies to devDependencies in root package.json.

Build: zero TS errors. Tests: 21/21 pass.

BREAKING CHANGE: createOpenApiResource now returns a callable function instead
of an object with .get(), .post(), .put(), .patch(), .delete() methods.

Before:
  const api = createOpenApiResource<paths>({ baseUrl });
  api.get('/pets', () => ({ params: { query: { limit: 10 } } }));
  api.post('/pets', () => ({ body: { name: 'Buddy' } }));

After:
  const api = createOpenApiResource<paths>({ baseUrl });
  api('/pets', () => ({ params: { query: { limit: 10 } } }));
  api('/pets', () => ({ body: { name: 'Buddy' } }), { method: 'post' });

The method option is type-safe: only methods defined for the given path
are allowed (autocomplete shows valid methods per path).

This aligns with Angular's guidance that httpResource should be used for
reads; mutations should use OpenApiHttpClient instead.
@luchsamapparat
luchsamapparat marked this pull request as ready for review July 17, 2026 10:57
@luchsamapparat
luchsamapparat merged commit 4927a3e into main Jul 17, 2026
4 checks passed
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.

3 participants