-
Notifications
You must be signed in to change notification settings - Fork 471
Tech debt: Refactor Event Handler REST API and OpenAPI internals #8096
Description
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.pyhas 3,523 lines with a single class (ApiGatewayResolver) spanning 1,430 lines and 38 methodsopenapi/params.pyhas ~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 PLR0912and# noqa PLR0911suppressions hiding real complexity - The
Routeclass 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:
- Create
openapi/schema_generator.pywith a class that takes a Route - Move
_get_openapi_path,_openapi_operation_metadata,_openapi_operation_parameters,_openapi_operation_request_body,_openapi_operation_returnto the new class - Route delegates to the generator
- 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:
- Move the full
__init__to Param base class - Subclasses override only what differs (default value, alias behavior)
- Keep class names and import paths identical
- 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:
- Create a factory function that produces the decorator method for a given HTTP method
- Register all 7 methods in BaseRouter using the factory
- Preserve type hints and docstrings for IDE support
- 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:
- Create
PROXY_EVENT_MAP: dict[ProxyEventType, type[BaseProxyEvent]]at module level - Replace the if-chain with a dict lookup
- 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:
- Create dispatch dicts for exact type matches and isinstance checks
- Refactor the function to use dispatch instead of cascading isinstance
- 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:
- Extract common init logic
- Each resolver class becomes a thin shell that sets its proxy type
- No changes to public imports or class names
Acknowledgment
- This request meets Powertools for AWS Lambda (Python) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Java, TypeScript, and .NET
Metadata
Metadata
Assignees
Labels
Type
Projects
Status