Developing Swift HTTP Lambdas Locally
When developing AWS Lambda functions, there are multiple ways to trigger it once you deploy it. In AWS’s lingo, these are called events. For example, you might want S3 events to trigger your Lambda function when a new file is uploaded. Or maybe whenever a new message is sent to an SQS queue. One very popular use case is to connect a Lambda function to API Gateway (or enable the function URL), so that you can trigger it via HTTP requests, such as webhooks, or regular API calls.
Unfortunately, the Swift serverless tooling ecosystem is not as mature as other languages when it comes to local development. To test your HTTP Lambda functions locally, you usually need one of the choices below:
- use a proxy server that simulates the API Gateway, such as the AWS Lambda Runtime Interface Emulator. It converts HTTP requests into the events that your Lambda function expects.
- call the invoke the
/invoke
endpoint locally, which requires you to provide the exact payload that the Lambda function expects.
Although these options work, they are not straightforward and require some setup, dealing with environment variables, or installing additional tools.
Fortunately, when developing Lambda functions that are aimed to be used as HTTP APIs, there’s a better way, and this post will explore this choice, along with its pros and cons.
The Hummingbird Lambda Package
One option when building a Lambda is to use only the Swift AWS Lambda Runtime and the Swift AWS Lambda Events packages. When you do so, it’s your responsibility to transform the event (from API Gateway, S3, SQS, and so on) into the payload that your Lambda function expects. When dealing with HTTP APIs, this will involve converting the APIGatewayV2
event properties into HTTP request properties.
Doing so manually is totally fine in some cases. Now, if your Lambda is handling requests with multiple paths and expects different body types, you’ll also need to implement some sort of routing logic. You can do so by yourself to keep the Lambda smaller.
Another good option, though, is to use the Hummingbird Lambda package. In that regard, here are the advantages of using it:
- It takes care of the event transformation, from an API Gateway event to a Hummingbird HTTP request;


- You initialize a Hummingbird
Router
, configure it as you would do for a regular Hummingbird server, and provide it to the Lambda handler;


With this setup in place, the Hummingbird Lambda package will forward the events to your router, and take care of converting the response back into the API Gateway event format as well.
A Standard Hummingbird Server
In a regular, plain Hummingbird server, here is what you would probably have in the same executable:
- Server initialization code, including the port and hostname;
- Routing logic
- Controllers
- Models


Now, if you want to convert it to work with Lambda, this is possible, but will require some changes, that will make your codebase more modular.
Extracting the Routing Logic into a Separate Target
The following approach will work both for a new project, or for an existing app that uses Hummingbird as described in the previous section. Instead of having everything in a single target, you can define the routing logic, controller and models into a separate target.
This is similar to a modular architecture on iOS. For example, you can have multiple feature targets, and the role of the iOS app target is to glue them together, being only a “shell” target, taking care of the app lifecycle and configuration.
Doing so, you would have 3 targets in your package description:
- A shared target containing the routing logic (and controllers and models);
- A target where you initialize a Hummingbird
Application
, initializing it with the shared router. you can run this server locally, or even deploy it as a regular server; - The Lambda target, which uses Hummingbird Lambda and the shared router to respond to requests;


Package Definition
Here is what the Package.swift file would look like:
let package = Package(
name: "my-server-app",
...
dependencies: [
.package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.10.0"),
.package(url: "https://github.com/hummingbird-project/hummingbird-lambda.git", from: "2.0.0-rc.4"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-events", from: "1.0.0"),
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime", exact: "1.0.0-alpha.3")
],
targets: [
.target(
name: "App",
dependencies: [
.product(name: "Hummingbird", package: "hummingbird"),
]
),
.executableTarget(
name: "LocalServer",
dependencies: [
.product(name: "Hummingbird", package: "hummingbird"),
.target(name: "App")
]
),
.executableTarget(
name: "Lambda",
dependencies: [
.product(name: "HummingbirdLambda", package: "hummingbird-lambda"),
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
.target(name: "App")
]
)
]
)
Notice how the app target is a dependency of both the LocalServer and the Lambda executable targets. Also, the local server depends on Hummingbird, and the Lambda target depends on Hummingbird Lambda and AWS Lambda Events.
Providing the Router to Both Targets
In your shared target, you can define a function that receives a routes, and configures it with the different controllers:
Configure.swift
import Hummingbird
public func configure<Context: RequestContext>(
router: Router<Context>,
configuration: Configuration // Any extra configuration you might want
) {
router.get("/hello") { _, _ in "Hello World!" }
// Add your controllers here
UsersController().addRoutes(to: router)
}
Here’s what UsersController
could look like:
import Hummingbird
public struct UsersController: RouteCollection {
func addRoutes(to router: Router<Context>) {
router
.group("users")
.post(use: create)
.get(":id", use: getUser)
}
}
Then, you can use the router configuration in your local server and Lambda targets:
LocalServer.swift
import App
import Hummingbird
@main
struct Server {
static func main() async throws {
// 1
let router = Router()
// 2
configure(router: router)
// 3
try await Application(
router: router,
configuration: .init(
address: .hostname("127.0.0.1", port: 8080),
serverName: "My-Local-Server"
)
)
.runService()
}
}
This is what this main function does:
- Creates a router;
- Uses the shared configuration function to configure the router;
- Initializes the application with the router and runs it;
And here’s what the file containing the Lambda function looks like:
Lambda.swift
import App
import AWSLambdaRuntime
import AWSLambdaEvents
import HummingbirdLambda
@main
struct Lambda: APIGatewayV2LambdaFunction {
typealias Context = BasicLambdaRequestContext<APIGatewayV2Request>
init(context: LambdaInitializationContext) async throws {}
func buildResponder() -> some HTTPResponder<Context> {
// 1
let router = Router(context: Context.self)
// 2
configure(router: router)
// 3
return router.buildResponder()
}
}
Notice how this file is very similar to the LocalServer.swift_** file:
- Create a router
- Use the shared configuration function to configure the router
- Return the HTTPResponder to be used by the LambdaHandler
With this setup, you can run the server locally, and also deploy the Lambda function to AWS Lambda, using the same logic behind the scenes.
Final Considerations
In many situations, Lambdas are actually not the best fit. They are great for handling requests that should result in a quick response, but they are probably not the best choice for handling long-running tasks, or if your server has a constant and steady load. Also, if your server needs to provide a websocket connection to the client, Lambdas are out of the question.
The approach described here is also great if, at some point, your app grows and you want to deploy it to a regular server, or even to a different cloud provider. As you already have the routing logic backed by Hummingbird and models in a separate target, you can deploy new targets with a different Application
initialization to the provider of your choice.
In the other hand, as mentioned in one of the previous sections, if your Lambda is expecting only a few paths and body types, choosing Hummingbird Lambda might be overkill. In that case, you can just use the AWS Lambda Runtime and the AWS Lambda Events package to handle the HTTP requests.
Explore Further
If you’re interested in learning more about Hummingbird and Lambda, you can read more about it:
Thanks to Joannis Orlandos and Mahdi Bahrami for their help while writing this post.
If you have any questions or suggestions, feel free to reach out on Mastodon or X.
See you at the next post. Have a good one!