Published: Jun 14, 2026
Building Restly: Integrating with daveshanley's Vacuum OpenAPI Linter

Restly aims to simplify the creation of OpenAPI spec files. Its GUI form-based editor is designed to enable API designers and engineers to create or edit OpenAPI files without wrestling with JSON and YAML.
Part of crafting an OpenAPI file is conforming to your API's rules and style guide. For example, "We use kebab-case for API paths," "We use camelCase for component properties," or "We require 'x-' prefixes on all our custom headers." As an API gains more contributors and grows in functionality through additional operations and components, a Spectral ruleset becomes essential for enforcing its style guide.
Having an OpenAPI linter running as you make changes to your OpenAPI file will help you catch problems early. You can also use the linter 's real-time results to progressively address issues. While integrating an OpenAPI linter into Restly proved critical, it presented some interesting, unforeseen challenges.
We have an article dedicated to discussing what OpenAPI linting is and why you should use it. In this article, however, we'll focus on the technical obstacles Restly faced when integrating with the de-facto open-source tools for OpenAPI linting: Stoplight Spectral and Vacuum.
Deciding on an OpenAPI Linter
Stoplight Spectral is the most popular OpenAPI linter. It has been around since late 2018, and is well tested and supported by Smartbear (who acquired Stoplight). Written in Typescript/Javascript, Stoplight Spectral would have been an easy choice to integrate with Restly, whose browser client is written in React-Typescript.
However, we also wanted to run any OpenAPI linter we selected server-side. Restly plans on building a Git integration to both push and pull OpenAPI changes directly from repositories. Restly's backend is written in Go, and maintaining our development speed and predictability relies on keeping our tech stack simple. We wanted to avoid adding a Javascript environment.
Conveniently, another OpenAPI linter exists that fully supports the Spectral ruleset and is written in Go: Vacuum. In addition to being a better fit with our backend infrastructure, Vacuum's performance improvement over Stoplight Spectral is an added bonus that would significantly benefit Restly's users. Vacuum also includes a developer API guide that helped us get started quickly.
Now we faced the opposite issue: Vacuum is not written in Typescript/Javascript, which meant integration with our browser client would not be as straightforward. Fortunately, it is straightforward to use Go binaries on the web by compiling them to WASM. This presented a more straightforward path to integrating Vacuum with our entire tech stack than Stoplight Spectral.
Building a Vacuum WASM Binary
Getting the first WASM binary built was problematic due to Vacuum's dependency with the Doctor, an open-source online toolset for OpenAPI spec editing and analysis. Some imports from the Doctor included SQLite dependencies that did not support compiling to WASM.
After going back and forth debating forking the project to manage these dependencies ourselves, we discovered that locking the dependency version of the Doctor to v0.0.66 seemed to work. This is not a long-term fix, but for right now, we were happy we got the binary build working!
Once that was resolved, the actual binary code was not very complex. We have our first version shown below.
With everything in place, we built the WASM binary using:
And looked at the results:
Our binary output was 55MB. This is a massive binary for web standards. However, let's not worry about this problem right now. We were still in the PoC stage and wanted to get a running integrated version of the WASM into Restly.
Using the Vacuum WASM Binary on Web
Go has an excellent wiki on using Go WASM binaries on web. We included the wasm_exec.js script to make working with Go WASM binaries easy in the header:
And instantiated our binary like so:
And after some plumbing, we were able to run the WASM binary on web in Restly! Below was outputted to the console.
Now that we've completed the PoC, it was time to tackle two major issues with this integration.
- Running the WASM directly on the main thread to lint an OpenAPI file would block the UI, necessitating integration with web workers to run Vacuum in the background.
- The binary 's large size. As Restly is hosted on Cloudflare, assets exceeding 25MB are not accepted.
Using Browser Workers
We needed Vacuum to run in the background for 2 reasons:
- Vacuum may take a few seconds to run, and completely blocking the UI was absolutely non-negotiable.
- We wanted the option to stop Vacuum from running when a user begins to make changes again.
To do this, we used web workers to trigger background processing of Vacuum, and used the terminate API to stop Vacuum from running (as far as we knew, there was no elegant way to stop Vacuum in the middle of linting).
The result is the following web worker. At this point, we updated our Vacuum binary to support ingesting a custom Spectral Ruleset via lintJsonWithRuleset:
We then triggered the worker like so:
And if we needed to end the worker (when a user made a change to the OpenAPI file) we would call:
Always re-initializing the worker to end a job is extreme and results in unnecessary allocations. However, we had no other way to force-stop the Vacuum analyzer mid-analyze, and real-world testing showed that the time to restart the worker and to reinstantiate the WASM binary was insignificant.
Investigating the Large WASM Binary
With Vacuum elegantly integrated with Restly, we needed to address a binary size issue. Cloudflare's 25MB upload limit meant we had to shrink the 55MB binary to under 25MB for elegant integration into our browser client.
Our first step was to analyze the WASM binary to identify what was bloating its size. Running the following command, we can visualize the size of each section within the WASM binary (as defined by its header):
The Code section is about 29MB. Go binaries are generally large because they include the entire Go runtime, supporting functionalities such as goroutines.
However, the more interesting part is the 28MB Data section. The WASM module should not contain any embeds or artifacts that would bloat Data to such a large size. To see what is stored in the Data section, we ran:
This command exports the data section, in both hex and string formats, to govacuum.data.txt. Upon examining the output, we found large sections of blank or repeating data:
Some areas are more pattern-like, with repeating blocks of 0000 and entries every N segments. This means large blocks of uninitialized data are being set. After some testing, it is clear that something within Vacuum is resulting in such a large WASM binary.
Is this a sign that we should fork Vacuum and strip unnecessary entries that are causing this bloat? Should we look back and see if there is an elegant integration with Stoplight Spectral? Should we just bite the bullet and host the 55MB file somewhere and require it to be downloaded on all clients? Actually, that last one seems a lot more practical than you would expect.
Going with hosting the 55MB Binary
Amidst all the discussion, and in the spirit of "getting things to work as a first iteration," we decided to host govacuum.wasm in our backend as a static asset. Instead of including it within the frontend's code repository and tightly coupled, the binary would be served as a static asset from our backend, which had no file size limit. The first time we ran this on our production instance, we were in for a pleasant surprise:
In a production instance, over the wire, the browser needed to download a ~10MB asset (not a 55MB asset). This is still large for web standards, but for a 10Mb/s internet connection, the asset could be downloaded in about 10 seconds. Thank you gzip!
We mentioned earlier that we saw large blocks of empty or repeating data segments. The benefit of such blocks is that they increase the compression ratio dramatically; large files can be compressed into much smaller files. For govacuum.wasm, we saw a compression ratio of 11:2 (a 5.5x reduction).
With this in mind, we needed to make a few more modifications to provide a great experience for our production users.
Versioning, Caching, and Eager Loading
With govacuum.wasm hosted as a static asset from our backend, a few more adjustments were needed to make this setup production-ready:
Versioning the WASM Binary
Since we would be making changes to govacuum.wasm regularly, we needed a way to version those static assets. For now, we decided on a basic incrementing ID for newer versions of the asset, starting with govacuum.1.wasm. When any change was introduced, we incremented this value to govacuum.2.wasm, hosted both the older and newer binary, and then updated the frontend to use the newer version. We can probably automate this at some point in the future, but for now this works like a charm.
Browser Caching
Since we version our binary as described above, we know that a binary with a specific ID would be immutable; it would never change, ensuring consistency. Therefore, we can instruct the browser to cache the file for us using the following header:
As a result, the cost of downloading our 10MB binary would only be incurred once (well, once every 90 days using max-age=7776000).
Eager Loading
Lastly, to provide the best experience for our users, we implemented eager loading to load the binary as early as possible when the page loads; even before the OpenAPI editor is opened. As a result, the user does not experience the ~10-second download time penalty. (Although, if a user jumps straight into the OpenAPI editor, and a new version of the binary is available, they may sense it then. Let's not get caught up in the most nuanced low-probability cases.)
Vacuum Integrated with Restly
It is very exciting to have an OpenAPI linter integrated with our browser client. You can try it today using the default Vacuum-recommended rulesets, or set up your own rules as well. There is much more we want to achieve; for example, integration with Git for automatic linting, tracking scores over time, ruleset verification, and recommended fixes. However, for now, we have reached a very exciting milestone!