Serverless computing has transformed the way we approach delivering seamless, responsive, and cost-effective services to customers. For users, it means accessing applications that adapt instantly to demand, provide more reliability, and often deliver these benefits at a lower cost.
To illustrate the advantages of serverless, imagine the following code snippet is all that you need to deploy an infinitely scalable, cost efficient API.
const app = new App();
const db = new Db();
app.get("/posts", async (req, res) => {
const posts = await db.posts.findAll();
res.send(posts);
});
export default app;
In this example, we’ve defined a posts API that fetches posts from a database and returns them to the user. Arguably all web applications boil down to this simple pattern: fetch data, transform it, and return it to the user.
But depending on your serverless provider, you will need to adjust your mental model around their runtime.
For example in AWS Lambda, you need to export a handler function that receives an event and context object. The event object contains information about the request, and the context object contains information about the runtime environment.
export default async function handler(event, context) {
switch (event.httpMethod) {
case "GET":
switch (event.path) {
case "/posts":
const posts = await db.posts.findAll();
return {
statusCode: 200,
body: JSON.stringify(posts),
};
break;
default:
return {
statusCode: 404,
body: JSON.stringify({ message: "Not Found" }),
};
}
break;
default:
return {
statusCode: 405,
body: JSON.stringify({ message: "Method Not Allowed" }),
};
}
}
Much uglier!
It would be nice if we could write our serverless functions in a way that abstracts away the runtime details, and allows us to focus on the business logic.
Enter Hono: a fast, lightweight web application framework built on web standards that supports any JavaScript runtime.
With Hono, the previous example can be written as:
import { Hono } from 'hono'
import { handle } from 'hono/aws-lambda'
import {db} from from './db';
const app = new Hono()
app.get('/posts', (c) => c.json(db.posts.findAll()))
export const handler = handle(app)
Much closer to the original example!
However, there’s one more detail we need to address. We don’t always have guarantees around function reuse in serverless environments. This means that we can’t rely on global state, and we need to be careful about how we manage connections to databases, caches, and other resources.
A popular pattern to address this is to offload your database requests to a proxy that maintains its own connection pool.
Neon, a serverless postgres offering provides this out of the box.
// db.js
const { neon } = require("@neondatabase/serverless");
const { PGHOST, PGDATABASE, PGUSER, PGPASSWORD } = process.env;
const db = neon(
`postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}?sslmode=require`
);
With these two files, we can run on any serverless platform for infinitely scalable, cost efficient APIs.
In a future post, we’ll explore limitations of different platforms when building more complex applications that may require background jobs, websockets, or long running processes.