Core package for Cloud Functions services
This package contains some core functionality for Node.js services running on Google Cloud Run via Cloud Functions, such as logging and error handling.
Development
pnpm install
pnpm test
Publishing the package:
pnpm version patch
pnpm publish
Initialization
npm install @google-cloud/functions-framework @biblioteksentralen/cloud-functions-core
To create a new Express app for Cloud Functions:
import { http } from "@google-cloud/functions-framework";
import { createApp } from "@biblioteksentralen/cloud-functions-core";
import { z } from "zod";
const projectId = z.string().parse(process.env.GCP_PROJECT_ID);
const app = createApp({ projectId });
http("my-cloud-function", app);
This configures the app with a logging middleware that configures Pino for Cloud Logging and includes trace context.
The logger is added to the Express Request context, so it can be used in all request handlers like so:
app.get("/", async (req, res, next) => {
req.log.info(`Hello log!`);
});
The app is also registered with the Cloud Functions framework.
Tip: Formatting logs with pino-pretty in development
When developing, pino-pretty can be used to format logs in a more human readable way.
Since cloud-functions-core
configures Pino for Google Cloud Logging, we need to explicitly tell pino-pretty about which keys we use with the flags --messageKey
and --levelKey
.
Here's a package.json
example (remember to npm i -D pino-pretty
):
{
"scripts": {
"dev": "functions-framework --target=fun-service --signature-type=http | pino-pretty --colorize --messageKey message --levelKey severity"
}
}
It's also possible to create a .pino-prettyrc
file if the script definition becomes too convoluted.
Error handling
This package provides an errorHandler
middleware for error handling in Express 4 inspired by How to Handle Errors in Express with TypeScript. It includes the express-async-errors
polyfill to support throwing errors from async request handlers, a feature that will be supported natively in Express 5.
import { http } from "@google-cloud/functions-framework";
import { createApp, errorHandler } from "@biblioteksentralen/cloud-functions-core";
import { z } from "zod";
const projectId = z.string().parse(process.env.GCP_PROJECT_ID);
const app = createApp({ projectId });
http("my-cloud-function", app);
app.use(errorHandler);
The package provides AppError
, a base class to be used for all known application errors (that is, all errors we throw ourselves).
import {
AppError,
RequestHandler
} from "@biblioteksentralen/cloud-functions-core";
export const defaultRequestHandler: RequestHandler = async (req, res, next) => {
throw new AppError('🔥 Databasen har brent ned');
};
As long as errors are thrown before writing the response has started, a JSON error response is produced on the form:
{ "error": "🔥 Databasen har brent ned" }
By default, errors based on AppError
are considered operational and displayed in responses. If an error should not be displayed, set isOperational: false
when constructing the error:
throw new AppError('🔥 Databasen har brent ned', { isOperational: false });
This results in a generic error response (but the original error is still logged):
{ "error": "Internal server error" }
A generic error response will also be shown for any unknown error, that is, any error that is not based on AppError
) is thrown. All errors, both known and unknown, are logged.
By default, errors use status code 500. To use another status code:
throw new AppError('💤 Too early', { httpStauts: 425 });
The package also provides a few subclasses of AppError
for common use cases, such as ClientRequestError
(yields 400 response) and Unauthorized
(yields 401 response).
throw new Unauthorized('🔒 Unauthorized');
Pub/Sub helpers
The package provides a helper function to parse and validate Pub/Sub messages delivered through push delivery.
The package is agnostic when it comes to which schema parsing/validation library to use.
We like Zod for its usability, and have added support for it in the errorHandler, but it's not very performant (https://moltar.github.io/typescript-runtime-type-benchmarks/), so you might pick something else if performance is important (if you do, remember to throw ClientRequestError
when validation fails).
import { z } from "zod";
import {
parsePubSubMessage,
Request,
Response,
} from "@biblioteksentralen/cloud-functions-core";
const messageSchema = z.object({
table: z.string(),
key: z.number(),
});
app.post("/", async (req: Request, res: Response) => {
const message = parsePubSubMessage(req, (data) => messageSchema.parse(JSON.parse(data)));
});