Warning
This is a work in progress and is not published yet.
- An opinionated, batteries-included framework for building serverless functions.
- Built on top of Hono
- Heavily based on async contexts
- Polished APIs
- Includes everything you need for a modern serverless app
- waitUntil functions
- Typed environment variables handling
- A standard way to throw and handle errors
- Observability
- Error reporting
- Tracing through OpenTelemetry
- Source maps
- Server-Timing
- Testing
- Per-test context
- Mocks
- Cache (optional)
- Vendor neutral
- Adapters for Cloudflare, AWS Lambda, Deno, etc.
- Context passing in other frameworks is broken
- We're all solving the same problems: error handling and reporting, logging, speed optimization, testing, etc.
Underpinning all of Wolk are Async Contexts. These make it possible to have global variables, but scoped to the current request.
Take this example: we want every function in our app to have access to the current user. We cannot set it globally, because requests might come in concurrently. We also don't want to pass it around to every function, because that's tedious and error-prone.
type User = { id: string; name: string };
const userContext = createAsyncContext<User>("user");
function currentUser() {
return userContext.consume();
}
export default {
fetch(req: Request) {
const user = await auth(req.headers.get("Authorization"));
return userContext.provide(user, () => route(req));
},
};
async function route(req: Request) {
return new Response("Hello, " + currentUser()?.name ?? "stranger");
}
In most apps, you don't want to wait for all async operations to finish before returning a response. That's why waitUntil
exists. On platforms like Cloudflare Workers, it's attached to the execution context object, so you have to pass it around. Wolk makes it available on an async context.
import { waitUntil } from "wolk";
function count(): void {
waitUntil(fetch("https://counter.com?increment=1"));
}
In the above example, running count
multiple times will result in multiple requests. It's more efficient to batch them:
import { waitUntil, onTeardown } from "wolk";
const countContext = createAsyncContext<{ count: number }>("count");
function count(): void {
countContext.consume().count++;
}
export default {
fetch(req: Request) {
countContext.provide({ count: 0 }, () => {
onTeardown(() => {
waitUntil(
fetch(`https://counter.com?increment=${countContext.consume().count}`)
);
});
waitUntil(
(async () => {
await delay(1000);
count();
count();
})()
);
return route(req);
});
},
};
In the above example, the count
function is called twice, but the request to https://counter.com?increment=2
is only made once.
onTeardown
is called after all the called waitUntil
s are resolved.
These functions also support nesting, so you can have multiple levels of teardown functions.
env
has three methods: get
, getOptional
, and isTrue
(checks if the value is "true")
export const [env, provideEnv] = createEnvContext<Env>();
type Env = { API_KEY: string };
export default {
fetch(req: Request, env: Env) {
provideEnv(env, () => route(req));
},
};
function route(req: Request) {
return new Response(env.get("API_KEY"));
}
Wolk provides a standard way to throw and handle errors. This makes it easy to report errors to your error tracking service, and to handle them in a consistent way.
You can attach information to errors, which will be passed to your error tracking service. You can also mark error info as public, which will be returned to the client.
import { WolkError } from "wolk/errors";
export class TrackingError extends WolkError.extend({
name: "TrackingError"
});
export class ApiTrackingError extends TrackingError.extend({
name: "ApiTrackingError"
});
export class ValidationTrackingError extends TrackingError.extend({
name: "ValidationTrackingError",
infoIsPublic: true,
httpStatus: 400,
});
async function trackOrder(orderId: string) {
if (!orderId) {
throw new ValidationTrackingError({
message: "Missing orderId",
info: {
orderId,
}
});
}
const response = await fetch(`https://tracker.com/orders/${orderId}`);
if (!response.ok) {
throw new ApiTrackingError({
message: "API error",
info: {
response: {
status: response.status,
body: await response.text(),
},
request: {
orderId,
}
}
})
}
return response.json();
}
Info can also be typed.
import { WolkError } from "wolk/errors";
export class UserError extends WolkError.extend<{ userId: string }>({
name: "UserError"
});
Wolk provides a simple API for reporting errors to your error tracking service.
Because the errors are different classes, it's easy to distinguish between them in your error tracking service.
import { report } from "wolk/observability";
try {
await trackOrder("123");
} catch (error) {
report(error);
}
The OpenTelemetry API is tedious and unclear, so Wolk provides a simple API for tracing.
import { trace } from "wolk/observability";
function fetchOrder(orderId: string) {
return trace({
name: "fetchOrder",
attributes: {
"request.orderId": orderId,
},
}, async () => {
const response = await fetch(`https://api.com/orders/${orderId}`);
return response.json();
});
}