Skip to content

Tech debt: Refactor Event Handler REST API and OpenAPI internals #8096

@leandrodamascena

Description

@leandrodamascena

Why is this needed?

I've been wanting to do this for a long time. The Event Handler REST API and OpenAPI modules have grown organically over the past years and accumulated significant technical debt. The code works and is well tested, but it's becoming harder and harder to add new features, review PRs, and onboard contributors.

Some numbers to illustrate:

  • api_gateway.py has 3,523 lines with a single class (ApiGatewayResolver) spanning 1,430 lines and 38 methods
  • openapi/params.py has ~450 lines of copy-pasted __init__ signatures across Path, Query, Header, Cookie, Body, Form, and File
  • 7 HTTP method decorators (get, post, put, delete, patch, head, options) are nearly identical, totaling ~350 lines
  • Multiple # noqa PLR0912 and # noqa PLR0911 suppressions hiding real complexity
  • The Route class mixes route configuration with OpenAPI schema generation (662 lines)

None of this is broken, but it slows us down. Every time we add a parameter to a decorator or a new param type, we have to touch 5+ places. Every time we touch OpenAPI generation, we risk breaking something in a 158-line method.

This is an internal refactor only. No breaking changes to the public API.

Which area does this relate to?

No response

Suggestion

Split the work into independent PRs that can be reviewed and merged separately. Each one should be safe on its own.

1. Extract OpenAPI schema generation from Route

Route._get_openapi_path() is 158 lines with deeply nested branches. All the schema generation logic can move to a dedicated class (e.g. RouteSchemaGenerator) that receives a Route and produces the OpenAPI path spec. The Route class drops from 662 to ~400 lines.

Steps:

  1. Create openapi/schema_generator.py with a class that takes a Route
  2. Move _get_openapi_path, _openapi_operation_metadata, _openapi_operation_parameters, _openapi_operation_request_body, _openapi_operation_return to the new class
  3. Route delegates to the generator
  4. No public API changes

2. Consolidate parameter class constructors

Path, Query, Header, Cookie all inherit from Param but each one copies the entire __init__ with 25+ parameters. The __init__ can live only in Param. Subclasses just set in_ and override specific behavior (e.g. Header has convert_underscores).

Same story for Body/Form/File.

Steps:

  1. Move the full __init__ to Param base class
  2. Subclasses override only what differs (default value, alias behavior)
  3. Keep class names and import paths identical
  4. Verify Pydantic FieldInfo compatibility

3. Generate HTTP method decorators programmatically

get(), post(), put(), delete(), patch(), head(), options() in BaseRouter are 7 methods with identical signatures that just call self.route(rule, "METHOD", ...). A loop or factory can generate them.

Steps:

  1. Create a factory function that produces the decorator method for a given HTTP method
  2. Register all 7 methods in BaseRouter using the factory
  3. Preserve type hints and docstrings for IDE support
  4. Verify that Router and all resolver subclasses still work

4. Use dispatch dict for proxy event conversion

_to_proxy_event() has 7 sequential if/return statements (suppressed with # noqa PLR0911). A simple dict mapping ProxyEventType to event class replaces all of them.

Steps:

  1. Create PROXY_EVENT_MAP: dict[ProxyEventType, type[BaseProxyEvent]] at module level
  2. Replace the if-chain with a dict lookup
  3. Remove the noqa

5. Simplify jsonable_encoder

jsonable_encoder in openapi/encoders.py has 11 return paths. A type-based dispatch pattern (dict of type to encoder function) would make it more readable and extensible.

Steps:

  1. Create dispatch dicts for exact type matches and isinstance checks
  2. Refactor the function to use dispatch instead of cascading isinstance
  3. Remove the noqa

6. Simplify resolver subclasses

LambdaFunctionUrlResolver, VPCLatticeResolver, VPCLatticeV2Resolver, ALBResolver are thin wrappers that only pass a different ProxyEventType to ApiGatewayResolver.__init__. The internal implementation can use a factory while keeping the public classes unchanged.

Steps:

  1. Extract common init logic
  2. Each resolver class becomes a thin shell that sets its proxy type
  3. No changes to public imports or class names

Acknowledgment

Metadata

Metadata

Labels

tech-debtTechnical Debt tasks

Projects

Status

Working on it

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions