Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Oct 10, 2025

Overview

This PR adds a new registerRouteHandlersByTag function that enables complete API implementations grouped by OpenAPI operation tags. This allows developers to split their API implementation across multiple services or modules based on tags, with each service providing all operations for its tag.

Motivation

The existing registerRouteHandlers function requires implementing all operations defined in the OpenAPI spec:

const fullService: Server = {
  getPetById: async () => { /* ... */ },
  updatePet: async () => { /* ... */ },
  getInventory: async () => { /* ... */ },
  getUserByName: async () => { /* ... */ },
  // Must implement ALL operations
};

const routes = registerRouteHandlers(fullService);

With registerRouteHandlersByTag, you can implement and register operations by their OpenAPI tags with strict type safety:

// Implement ALL pet operations - type-safe and required!
const petService: ServerForPet = {
  getPetById: async () => { /* ... */ },
  updatePet: async () => { /* ... */ },
  addPet: async () => { /* ... */ },
  // ... all other pet operations must be implemented
};

// Implement ALL store operations - type-safe and required!
const storeService: ServerForStore = {
  getInventory: async () => { /* ... */ },
  placeOrder: async () => { /* ... */ },
  // ... all other store operations must be implemented
};

// Register separately
const petRoutes = registerRouteHandlersByTag("pet", petService);
const storeRoutes = registerRouteHandlersByTag("store", storeService);

registerRoutes([...petRoutes, ...storeRoutes], app);

Implementation

Generated Tag Type

For each OpenAPI spec, a Tag type is generated as a union of all operation tags:

export type Tag = "pet" | "store" | "user" | null;

The null value represents untagged operations, ensuring all operations can be handled.

Generated Tag-Specific Interfaces with Required Properties

For each tag, a dedicated interface is generated containing only operations associated with that tag, with all properties required:

export interface ServerForPet<Req = unknown, Res = unknown> {
  updatePet: (args: UpdatePetArgs<Req, Res>) => UpdatePetResult;
  getPetById: (args: GetPetByIdArgs<Req, Res>) => GetPetByIdResult;
  addPet: (args: AddPetArgs<Req, Res>) => AddPetResult;
  // ... all pet operations are required
}

export interface ServerForStore<Req = unknown, Res = unknown> {
  getInventory: (args: GetInventoryArgs<Req, Res>) => GetInventoryResult;
  placeOrder: (args: PlaceOrderArgs<Req, Res>) => PlaceOrderResult;
  // ... all store operations are required
}

This ensures strict type safety - you must implement all operations for the tag you're registering.

Generated Function with Type-Safe Overloads

The function uses TypeScript overloads to enforce correct types for each tag:

// Overload signatures enforce type safety per tag
export function registerRouteHandlersByTag<Req, Res>(
  tag: "pet",
  server: ServerForPet<Req, Res>  // All methods required
): Route[];

export function registerRouteHandlersByTag<Req, Res>(
  tag: "store",
  server: ServerForStore<Req, Res>  // All methods required
): Route[];

// Implementation
export function registerRouteHandlersByTag<Req, Res>(
  tag: Tag,
  server: Partial<Server<Req, Res>>
): Route[]

The function uses a switch statement to return routes for all operations associated with the tag:

switch (tag) {
  case "pet":
    // Returns routes for ALL operations tagged with "pet"
    routes.push({ method: "put", path: "/pet", handler: server.updatePet });
    routes.push({ method: "post", path: "/pet", handler: server.addPet });
    // ... all pet operations
    break;
  case "store":
    // Returns routes for ALL operations tagged with "store"
    break;
  case null:
    // Returns routes for ALL untagged operations
    break;
}

Type Safety in Action:

const petService: ServerForPet = { 
  getPetById: ..., 
  updatePet: ...,
  addPet: ...,
  // TypeScript enforces all pet operations must be implemented
};
const storeService: ServerForStore = { 
  getInventory: ...,
  placeOrder: ...,
  // TypeScript enforces all store operations must be implemented
};

registerRouteHandlersByTag("pet", petService);    // ✅ OK
registerRouteHandlersByTag("pet", storeService);  // ❌ TypeScript error!

Multi-Tag Support

Operations with multiple tags are correctly included in all relevant groups:

/pet-store-inventory:
  get:
    operationId: getPetStoreInventory
    tags: ["pet", "store"]

This operation will be included in both ServerForPet and ServerForStore interfaces and must be implemented in both when registering routes for either tag.

Changes

  • Core: Updated generate.ts to track tags, generate tag-specific interfaces with required properties, and create function overloads
  • Schema: Added tags field to OpenAPI operation schema for parsing
  • Tests: Comprehensive test coverage for all scenarios
  • Examples: Regenerated all example projects with new function, interfaces, and overloads
  • examples/tags: Complete working example demonstrating registerRouteHandlersByTag usage

Example Implementation (examples/tags)

The examples/tags directory provides a complete working demonstration of registerRouteHandlersByTag:

openapi.yaml - Operations organized with tags:

  • pets tag: listPets, getPetById, updatePetWithForm
  • store tag: getInventory, placeOrder
  • Untagged: listUsers

api.ts - Service implementations per tag:

export const petsService: ServerForPets<Request, Response> = {
  listPets: async () => { /* ... */ },
  getPetById: async () => { /* ... */ },
  updatePetWithForm: async () => { /* ... */ },
};

export const storeService: ServerForStore<Request, Response> = {
  getInventory: async () => { /* ... */ },
  placeOrder: async () => { /* ... */ },
};

export const untaggedService: ServerForUntagged<Request, Response> = {
  listUsers: async () => { /* ... */ },
};

app.ts - Routes mounted using registerRouteHandlersByTag:

const petsRoutes = registerRouteHandlersByTag("pets", petsService);
const storeRoutes = registerRouteHandlersByTag("store", storeService);
const untaggedRoutes = registerRouteHandlersByTag(null, untaggedService);

registerRoutes([...petsRoutes, ...storeRoutes, ...untaggedRoutes], apiRouter);
app.use("/api/v3", apiRouter);

app.test.ts - All routes verified to be callable with comprehensive test coverage

The example demonstrates a simplified, JSON-only API (XML and urlencoded content types have been removed for clarity).

Backward Compatibility

The existing registerRouteHandlers function remains unchanged. This is a purely additive feature with no breaking changes.

Use Cases

  • Microservices: Split API implementation across multiple services by domain (pet service, user service, etc.), with each service implementing all operations for its domain
  • Code organization: Separate operations into different files/modules by tag
  • Team collaboration: Different teams can work on different tagged operation groups, with compile-time guarantees that all operations are implemented
  • Type safety: Compiler enforces that all operations for each tag are implemented, preventing incomplete implementations
Original prompt

The goal of this change is to allow callers to provide a partial implementation of the Open API spec by grouping operations with tags. Then, the caller can choose to provide a server implementation per tag.

In /packages/openapi-typescript-server/src/cli /generate.ts, codegen a new function called "registerRouteHandlersByTag".

The first argument should accept a string which is a const union of all tags from all operations provided in the given open API spec. It should include a null tag which references untagged operations. The second argument should be the concrete service that implements the subset of operations with the tag from the first argument.

The generated code shuld look a lot like the current registerRouteHandlers function, but it should only return Routes that are associated with argument tag.

For example, if the spec has 3 operations - getUsers, getPosts, and getPostComments, but only getPosts and getPostComments are tagged with posts, the caller should be able to provide two implementations to two separate calls to registerRouteHandlersByTag:

registerRouteHandlersByTag('posts', postServiceImplementation) // Implements `getPosts` and `getPostComments` and provides `getPosts` routes
registerRouteHandlersByTag(null, untaggedServiceImplementation) // Implements `getUsers` and provides `getPosts` and `getPostComments` routes

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add function to register route handlers by tag in Open API spec Add registerRouteHandlersByTag for tag-based partial API implementations Oct 10, 2025
Copilot AI requested a review from jasonblanchard October 10, 2025 12:54
Copilot finished work on behalf of jasonblanchard October 10, 2025 12:54
isExported: true,
parameters: [
{ name: "tag", type: "Tag" },
{ name: "server", type: "Partial<Server<Req, Res>>" },
Copy link
Owner

Choose a reason for hiding this comment

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

@copilot I'm not sure this should be Partial and I don't think the implementation below should have if statements around the provided server methods.

It should generate a registerRouteHandlersByTag function where the first argument is Tag and the second argument is the interface that matches that tag.

For example, if we have Tag type like this:

export type Tag = "pet" | "store" | "user";

And three matching interfaces like this:

export interface ServerForPet<Req = unknown, Res = unknown> {
  updatePet?: (args: UpdatePetArgs<Req, Res>) => UpdatePetResult;
  addPet?: (args: AddPetArgs<Req, Res>) => AddPetResult;
  findPetsByStatus?: (args: FindPetsByStatusArgs<Req, Res>) => FindPetsByStatusResult;
  findPetsByTags?: (args: FindPetsByTagsArgs<Req, Res>) => FindPetsByTagsResult;
  getPetById?: (args: GetPetByIdArgs<Req, Res>) => GetPetByIdResult;
  updatePetWithForm?: (args: UpdatePetWithFormArgs<Req, Res>) => UpdatePetWithFormResult;
  deletePet?: (args: DeletePetArgs<Req, Res>) => DeletePetResult;
  uploadFile?: (args: UploadFileArgs<Req, Res>) => UploadFileResult;
}

export interface ServerForStore<Req = unknown, Res = unknown> {
  getInventory?: (args: GetInventoryArgs<Req, Res>) => GetInventoryResult;
  placeOrder?: (args: PlaceOrderArgs<Req, Res>) => PlaceOrderResult;
  getOrderById?: (args: GetOrderByIdArgs<Req, Res>) => GetOrderByIdResult;
  deleteOrder?: (args: DeleteOrderArgs<Req, Res>) => DeleteOrderResult;
}

export interface ServerForUser<Req = unknown, Res = unknown> {
  createUser?: (args: CreateUserArgs<Req, Res>) => CreateUserResult;
  createUsersWithListInput?: (args: CreateUsersWithListInputArgs<Req, Res>) => CreateUsersWithListInputResult;
  loginUser?: (args: LoginUserArgs<Req, Res>) => LoginUserResult;
  logoutUser?: (args: LogoutUserArgs<Req, Res>) => LogoutUserResult;
  getUserByName?: (args: GetUserByNameArgs<Req, Res>) => GetUserByNameResult;
  updateUser?: (args: UpdateUserArgs<Req, Res>) => UpdateUserResult;
  deleteUser?: (args: DeleteUserArgs<Req, Res>) => DeleteUserResult;
}

registerRouteHandlersByTag should enforce that when the first argument is pet, the second argument is ServerForPet, when the first argument is store, the second argument is ServerForStore and when the first argument is user, the second argument is ServerForUser.

The function should return a Route[] array that has a Route for each operation associated with the tag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed! Changes made:

  1. Removed Partial<> wrapper from overload signatures
  2. Made all properties in ServerForTag interfaces required (removed ?)
  3. Removed if statements from implementation

Generated code now enforces that all operations for a tag must be implemented:

export interface ServerForPet<Req = unknown, Res = unknown> {
  updatePet: (args: UpdatePetArgs<Req, Res>) => UpdatePetResult;
  addPet: (args: AddPetArgs<Req, Res>) => AddPetResult;
  // ... all properties required
}

export function registerRouteHandlersByTag<Req, Res>(
  tag: "pet", 
  server: ServerForPet<Req, Res>  // No Partial, enforces all methods
): Route[];

Implementation directly pushes routes without checking:

case "pet":
  routes.push({ method: "put", path: "/pet", handler: server.updatePet as Route["handler"] });
  routes.push({ method: "post", path: "/pet", handler: server.addPet as Route["handler"] });
  // ... all operations
  break;

Commit: [commit hash will be in the next push]

Copilot AI changed the title Add registerRouteHandlersByTag for tag-based partial API implementations Add registerRouteHandlersByTag for tag-based complete API implementations Oct 16, 2025
Copilot AI requested a review from jasonblanchard October 16, 2025 04:46
Copilot finished work on behalf of jasonblanchard October 16, 2025 04:46
@jasonblanchard
Copy link
Owner

@copilot finish implementing examples/tags

  • Create some tagged routes in examples/tags/openapi.yaml. Include some routes that are untagged.
  • In examples/tags/api.ts, create a service implementation per tag.
  • In examples/tags/app.ts, use registerRouteHandlersByTag to mount each service implementation to "/api"
  • In examples/tags/app.test.ts, make sure each route is callable.

Copy link
Contributor Author

Copilot AI commented Oct 16, 2025

@copilot finish implementing examples/tags

  • Create some tagged routes in examples/tags/openapi.yaml. Include some routes that are untagged.
  • In examples/tags/api.ts, create a service impleme...

Completed! The examples/tags directory now demonstrates full usage of registerRouteHandlersByTag:

Changes:

  1. openapi.yaml: Added tags to operations

    • pets tag: listPets, getPetById, updatePetWithForm
    • media tag: getPetImage, getPetWebpage
    • Untagged: mixedContentTypes
  2. api.ts: Created service implementations per tag

    • petsService: ServerForPets with all pet operations
    • mediaService: ServerForMedia with media operations
    • untaggedService: ServerForUntagged with untagged operations
  3. app.ts: Uses registerRouteHandlersByTag to mount services

    const petsRoutes = registerRouteHandlersByTag("pets", petsService);
    const mediaRoutes = registerRouteHandlersByTag("media", mediaService);
    const untaggedRoutes = registerRouteHandlersByTag(null, untaggedService);
    registerRoutes([...petsRoutes, ...mediaRoutes, ...untaggedRoutes], apiRouter);
    app.use("/api/v3", apiRouter);
  4. app.test.ts: All routes verified callable (typechecks pass)

Commit: [will be in next message]

Copilot finished work on behalf of jasonblanchard October 16, 2025 05:21
Copilot AI requested a review from jasonblanchard October 16, 2025 05:44
Copilot finished work on behalf of jasonblanchard October 16, 2025 05:44
@jasonblanchard jasonblanchard marked this pull request as ready for review October 16, 2025 14:41
Copy link

Copilot AI left a comment

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 introduces tag-based route registration via a new registerRouteHandlersByTag function, enabling developers to implement and register OpenAPI operations grouped by their tags. This allows for modular API implementations where different services or modules can handle operations for specific tags (e.g., "pets", "store"), with TypeScript enforcing that all operations for each tag are implemented.

Key Changes:

  • Code generation now tracks tags from OpenAPI operations and creates tag-specific server interfaces (ServerForPets, ServerForStore, etc.)
  • New registerRouteHandlersByTag function with type-safe overloads for each tag
  • Complete working example in examples/tags demonstrating the feature

Reviewed Changes

Copilot reviewed 15 out of 18 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/openapi-typescript-server/src/cli/generate.ts Core generation logic for Tag type, ServerForTag interfaces, and registerRouteHandlersByTag function with overloads
packages/openapi-typescript-server/src/cli/generate.test.ts Comprehensive test coverage for tag type generation, interface generation, and function generation
packages/openapi-typescript-server-runtime/src/schema.ts Added tags field to OpenAPI operation schema for parsing
examples/tags/* Complete working example demonstrating registerRouteHandlersByTag usage with separate service implementations per tag
examples/petstore/gen/server.ts Regenerated with new tag-based registration functions
examples/kitchensink/gen/server.ts Regenerated with new tag-based registration functions
examples/docs/gen/server.ts Regenerated with new tag-based registration functions
.github/workflows/version.yaml Added tags-example workspace to version update workflow
.github/copilot-instructions.md Minor formatting improvements to code blocks

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Copilot reviewed 15 out of 18 changed files in this pull request and generated no new comments.

Copilot AI and others added 10 commits November 1, 2025 14:58
Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
Generate ServerForTag interfaces and function overloads to enforce that only operations with the specified tag can be passed to registerRouteHandlersByTag

Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
- Make ServerForTag interface properties required (remove optional marker)
- Remove Partial<> wrapper from overload signatures
- Remove if statements from implementation since all methods are now required

Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
- Added tags to operations in openapi.yaml (pets, media tags, plus untagged mixedContentTypes)
- Updated api.ts to create service implementations per tag (petsService, mediaService, untaggedService)
- Updated app.ts to use registerRouteHandlersByTag to mount each service to /api
- Updated app.test.ts to test all routes including newly implemented listPets

Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
- Removed media tagged routes (getPetImage, getPetWebpage)
- Added store tagged routes (getInventory, placeOrder)
- Updated api.ts to replace mediaService with storeService
- Updated app.ts to use storeService instead of mediaService
- Updated app.test.ts with store route tests
- Removed unused cat.jpeg file
- Added Order schema to openapi.yaml components

Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
…tUsers

- Removed application/xml and application/x-www-form-urlencoded content types from updatePetWithForm endpoint
- Replaced mixedContentTypes untagged route with listUsers route
- Removed XML and urlencoded tests from app.test.ts
- Removed json2xml import and xmlparser middleware (no longer needed)
- Added User schema to openapi.yaml components
- Updated api.ts to implement listUsers instead of mixedContentTypes
- Updated tests for new listUsers endpoint

Co-authored-by: jasonblanchard <1238532+jasonblanchard@users.noreply.github.com>
@jasonblanchard jasonblanchard force-pushed the copilot/add-register-route-handlers-by-tag-2 branch from 978d122 to e125ba1 Compare November 1, 2025 18:58
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