Introduction
Handling file uploads in serverless APIs sounds simple until you actually try to do it.
If you're building APIs with AWS Lambda Powertools and OpenAPI validation, you quickly run into a limitation:
multipart/form-data isn’t natively supported in the same way as JSON or form-encoded requests.
That gap forces teams into workarounds:
- Manual multipart parsing
- Base64 hacks
- Disabling validation entirely
None of which are ideal in production systems.
This article walks through:
- The real problem
- How the feature was designed
- How you can now use it in practice
The Problem: File Uploads Break the Abstraction
Before this feature, Powertools handled:
- JSON payloads
- Query parameters
- Headers
- Form data (
application/x-www-form-urlencoded)
But not:
multipart/form-data (file uploads)
That meant:
@app.post("/upload")
def upload(file: bytes):
...
simply didn’t work with OpenAPI validation.
Instead, developers had to:
- Parse raw request bodies manually
- Disable validation middleware
- Or redesign APIs around non-standard formats
At scale, this creates:
- Inconsistent APIs
- Security gaps
- Poor developer experience
The Goal: Make File Uploads First-Class
The aim was simple:
Make file uploads work the same way as
Query(),Header(), andForm()
That means:
- Type-safe
- Automatically validated
- Fully reflected in OpenAPI schema
- Works with Swagger UI
The Solution: File() Parameter Support
You can now define file inputs like this:
from typing import Annotated
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.openapi.params import File, UploadFile
app = APIGatewayRestResolver(enable_validation=True)
app.enable_swagger(path="/swagger")
@app.post("/upload")
def upload(file_data: Annotated[UploadFile, File(description="File to upload")]):
return {
"filename": file_data.filename,
"content_type": file_data.content_type,
"file_size": len(file_data),
}
Two Ways to Work with Files
1. Raw bytes
file: Annotated[bytes, File()]
You get:
- File content only
2. Rich file object
file_data: Annotated[UploadFile, File()]
You get:
- Content
- Filename
- Content type
This is usually what you want in real systems.
Combining Files with Form Data
@app.post("/upload-csv")
def upload_csv(
file_data: Annotated[UploadFile, File(description="CSV file")],
separator: Annotated[str, Form(description="CSV separator")] = ",",
):
text = file_data.content.decode("utf-8")
This unlocks:
- Metadata + file uploads
- Real-world API patterns
What Changed Under the Hood
Supporting this wasn’t just adding a new parameter type.
It required:
- Multipart parsing logic
- Boundary handling (including WebKit quirks)
- Base64 decoding for Lambda event payloads
- Differentiating file vs form fields
- OpenAPI schema generation (
format: binary) - Validation integration
There’s also a helpful runtime safeguard:
If multipart requests aren’t properly base64 encoded, a warning is emitted
This helps catch common misconfigurations early.
API Gateway Gotcha (Important)
If you're using REST API (v1), you must configure:
Globals:
Api:
BinaryMediaTypes:
- "multipart~1form-data"
Without this:
File uploads won’t work correctly.
For:
- HTTP API (v2)
- Lambda Function URLs
- ALB
It works out of the box.
Before vs After
Before
- Manual parsing
- No validation
- Custom schemas
- Inconsistent APIs
After
- Native
File()support - OpenAPI validation
- Swagger UI integration
- Cleaner, safer APIs
Why This Matters
This isn’t just about file uploads.
It’s about removing friction from real-world API design.
When basic capabilities are missing:
- Engineers build workarounds
- Systems become inconsistent
- Reliability suffers
By making file uploads a first-class feature:
- APIs become more predictable
- Validation becomes reliable
- Developer experience improves significantly
Open Source Insight
One interesting part of this work:
The implementation evolved through multiple iterations before reaching the final version.
Large features often:
- Start broad
- Get refined for maintainability
- Land as cleaner, more focused implementations
That process is what makes open source powerful —
it’s not just about shipping code, but improving it collaboratively.
Final Thoughts
If you’re building serverless APIs and dealing with file uploads:
You no longer need workarounds.
This feature brings Powertools closer to frameworks like:
- FastAPI
- Django
- Express
…but in a serverless-native way.
References
- Feature PR: https://github.com/aws-powertools/powertools-lambda-python/pull/8093
- Original implementation: https://github.com/aws-powertools/powertools-lambda-python/pull/7132
- Feature request: https://github.com/aws-powertools/powertools-lambda-python/issues/7124
Top comments (0)