DEV Community

Cover image for 🏎️💨 Turbocharge your builds with a Turborepo remote cache in a single edge function
Matt Kane
Matt Kane

Posted on

🏎️💨 Turbocharge your builds with a Turborepo remote cache in a single edge function

Monorepos are a great tool for organizing projects, but they can lead to slow build particularly in complex projects with lots of interdependent packages. Turborepo is a great solution for improving your build times which has become particularly popular for Next.js sites. It uses lots of clever tricks to ensure you only build the absolute minimum needed, using lots of caching and dependency tracking. I'm going to share how I built a custom Turborepo remote cache in a single edge function.

The secrets of Full Turbo

One of Turborepo's neatest tricks is a global remote cache, which lets you share build artifacts between your local development environment and production builds and between different members of your team. It works in the background to speed up your builds everywhere.

Turborepo is owned by Vercel, so understandably they want you to use their cache service. However Turborepo does support custom remote caching, and there is already a great open source Turborepo remote cache project, but I had a feeling I could build something simpler.

A cache server in 100 lines of code

While the remote cache server API is undocumented, it is fortunately quite simple. I had early access to the new Netlify Blobs product, which seemed perfect for this. With an Edge Function handling the API and Netlify Blobs for the storage, I implemented a Turborepo remote cache in under 100 lines of code.

If you just want to start speeding up your builds, you can check out the repo or just click the button: Deploy to Netlify
If you want to see how I built it, read on.

The Turborepo remote cache API

The docs for the Turborepo custom cache currently consists of a link to the Go source of the client, but the API is fortunately very simple. A Turborepo remote cache needs an endpoint at /v8/artifacts/:hash that accepts PUT requests to add an item, and GET requests to return it. Authentication is via a shared bearer token, and the teamId is passed as a query param. This is super simple to set up as a Netlify edge function:

import type { Config } from "@netlify/edge-functions";

export default async function handler(request: Request, context: Context) {
   // Do cool stuff here
}

export const config: Config = {
    method: ["GET", "PUT"],
    path: "/v8/artifacts/:hash",
    // This lets us handle our own cache rules
    cache: "manual",
};

Enter fullscreen mode Exit fullscreen mode

This gives us a function that runs on the right path, with the right methods.

Now add some auth. Turborepo sends the token in an Authorization header, which we can store in a shared environment variable. You can use anything for the token value. I like to use a UUID, which you can generate on a Mac by running uuidgen in the terminal. You'll need to store the same token on the server site and any repos that use it.

// snip
export default async function handler(request: Request, context: Context) {
  const bearerHeader = request.headers.get("authorization");
  // Remove the leading "Bearer " from the token
  const token = bearerHeader?.replace("Bearer ", "");
  // Compare it with the stored value, and return a 401 if it doesn't match
  if (!token || token !== Netlify.env.get("TURBO_TOKEN")) {
    console.log("Unauthorized");
    return new Response("Unauthorized", { status: 401 });
  }
}
Enter fullscreen mode Exit fullscreen mode

We're going to use Netlify Blobs to store our artifacts. These don't need any setup - you can just start using them. We'll use a store based on the team ID:

import { getStore } from "@netlify/blobs";
import type { Context } from "@netlify/edge-functions";

export default async function handler(request: Request, context: Context) {
  // Auth stuff goes here...

  const url = new URL(request.url);

  const teamId = url.searchParams.get("teamId") ?? "team_default";

  // Get a store
  const store = getStore(`artifacts-${encodeURIComponent(teamId)}`);

Enter fullscreen mode Exit fullscreen mode

Now we just need to handle getting and putting the artifacts. First we generate the cache key for the object and then handle uploads:

  const hash = context.params?.hash || url.pathname.split("/").pop();

  if (!hash) {
    console.log("Missing hash");
    return new Response("Not found", { status: 404 });
  }

  const key = encodeURIComponent(hash);

  if (request.method === "PUT") {
    // Get the uploaded file as binary
    const blob = await request.arrayBuffer();
    if (!blob) {
      console.log("No content");
      return new Response("No content", { status: 400 });
    }
    // ...then put it in the store. That's all!
    await store.set(key, blob);
    return new Response("OK");
  }
Enter fullscreen mode Exit fullscreen mode

Now we've implemented uploads, we can implement downloads.

  try {
    // Try to get the blob from the store as a binary buffer
    const blob = await store.get(key, {
      type: "arrayBuffer",
    });
    if (!blob) {
      // Oh no, return a 404
      return new Response(`Artifact ${hash} not found`, { status: 404 });
    }

    const headers = new Headers();
    // This content-type is required by Turborepo
    headers.set("Content-Type", "application/octet-stream");

    headers.set("Content-Length", blob.byteLength.toString());

    // We can set some useful cache headers, using Netlify's new cache features
    headers.set(
      "Netlify-CDN-Cache-Control",
      "public, s-maxage=31536000, immutable"
    );
    headers.set("Netlify-Vary", "header=Authorization,query=teamId");

    return new Response(blob, { headers });
  } catch (e) {
    // Catch any errors and return a 500
    console.log(e);
    return new Response(e.message, { status: 500 });
  }
Enter fullscreen mode Exit fullscreen mode

That's it!

If you add that to your site at netlify/edge-functions/turbofan.ts then it will run and handle requests to the cache URL.

Easy mode

Instead of manually writing or copying the code, you can just use the version I've published to deno.land:

import type { Config } from "@netlify/edge-functions";

export { handleRequest as default } from "https://deno.land/x/turbofan/mod.ts";

export const config: Config = {
    method: ["GET", "PUT"],
    path: "/v8/artifacts/:hash",
    cache: "manual",
};

Enter fullscreen mode Exit fullscreen mode

That's all you need. Make sure you've set the TURBO_TOKEN env var.

Or, even easier, just click this button to deploy a standalone remote cache:
Deploy to Netlify

Setting up your repo

You now need to tell turborepo to use the remote cache. You can do this by creating a config file. This is not the turbo.json in the root of your repo! You create it at .turbo/config.json, which may have been excluded in your .gitignore You will need to change that. Here's what you need to create:

{
    "teamid": "team_anything_you_want_here",
    "apiurl": "https://your-turboprop-server-name-here.netlify.app"
}
Enter fullscreen mode Exit fullscreen mode

If you create that and set the TURBO_TOKEN env var, Turborepo will know to use the remote cache.

You can see it's working because Turborepo will print "Remote caching enabled" in the logs.

Have fun, and may all your builds be FULL TURBO.

Top comments (0)