The Fastest Way to Build It Isn’t Always in the Final Stack
A vanilla JavaScript Greenhouse job board demonstrates why working integrations can be more useful than planning them inside a CMS.
- API Integration
- Greenhouse
- JavaScript
- Prototyping
- Software Architecture
The fastest way to build the real thing is not always to start in the real stack.
That sounds backward. If you already know the final implementation belongs in WordPress, a React application, or some other established platform, why spend time building something somewhere else?
Because with third-party integrations, the platform often isn’t the biggest unknown. The data is. And the behavior. And the handful of decisions that sound trivial until you’re looking at actual API responses in a browser.
I recently revisited an old Greenhouse job board integration that made this point for me.
From a jQuery integration to a working model
The original was a small jQuery implementation from an earlier project. It did what it needed to do: pull published jobs from Greenhouse, render a listing, let users filter the results, and hand applicants back to the applicant tracking system (ATS).
I cleaned it up and rebuilt it as a small vanilla JavaScript front-end. No framework and no CMS dependency. Just a configurable client of the Greenhouse Job Board API.
You can try the demo using Anthropic’s public job board, or look through the source on GitHub.
The idea is straightforward: provide a public board token, fetch published positions, normalize the response, and render a searchable job board with detail pages.
But the useful part isn’t that I can render some jobs. It’s that the implementation doesn’t need to know very much about any particular company’s job board.
Configuration instead of assumptions
The public Greenhouse API exposes job titles, locations, descriptions, URLs, and optional metadata. The catch is that metadata varies by organization. A field that matters to one company may not exist on another board, or it may be named differently.
That makes a lot of job board integrations deceptively brittle. It’s easy to hard-code a couple of filters for the first customer and then discover that the next board organizes its data differently.
In this implementation, the token, labels, metadata filters, and detail-rendering behavior are configuration. Here’s a shortened example using the actual class from the project:
new GreenhouseJobs({
boardToken: 'anthropic',
root: '#grnhse_app',
jobsPath: '/jobs/',
jobDetailRender: 'custom',
metaFields: []
}).init();
That starts with the core board data. Where Greenhouse exposes custom metadata, the metaFields configuration can map those fields to named columns and filters, without rewriting the rendering logic.
The demo itself also helps inspect the metadata available on a public board and choose which fields to expose. In other words, the prototype is useful for discovering the configuration, not just exercising it.
Normalize the data before building the interface
The Greenhouse API is reasonably approachable. Its public GET endpoints don’t require authentication, and the jobs endpoint can return descriptions, departments, and offices alongside the listing data.
But an API response is not yet an application data model.
A job title needs to be searchable. Locations need consistent values for filters. Optional metadata needs predictable keys. URLs need to survive a trip from listing to detail page and back.
The implementation handles that in a normalization step, rather than making every part of the UI interpret Greenhouse’s raw response independently:
normalizeJob(job) {
const metadata = this.getMetadataMap(job.metadata || []);
const exposedMeta = this.getExposedMeta(metadata);
return {
id: job.id,
title: job.title || '',
content: DomUtils.decodeEntities(job.content || ''),
absoluteUrl: job.absolute_url || '',
location: job.location && job.location.name ? job.location.name : '',
meta: exposedMeta,
searchableTitle: DomUtils.normalizeText(job.title || ''),
raw: job
};
}
That’s an excerpt from the actual source, not an imagined framework for a theoretical integration.
Once the client has a predictable shape, the rendering and filtering code can stay relatively boring. That’s what I want from an integration layer.
Decide where the ATS takes over
Another easy way to make a job board more complicated than necessary is to start rebuilding the application process.
Greenhouse already owns the recruiting workflow. My code doesn’t need to replicate application questions, uploads, consent requirements, or submission handling just because it owns the job listing experience.
The project supports two approaches:
| Experience | Responsibility |
|---|---|
| Custom listing and job detail | Render jobs, descriptions, search, and filters on the website |
| Greenhouse-hosted apply link | Hand the candidate back to the ATS when it’s time to apply |
| Greenhouse iframe mode | Embed the Greenhouse application experience rather than building a custom submission system |
The prototype also preserves gh_src tracking parameters and uses gh_jid in the URL to load job details. Those are small implementation decisions, but they’re exactly the sort of thing that gets missed when a third-party integration is specified only in screenshots or tickets.
A good integration doesn’t have to own the entire user journey. Sometimes the right decision is to own discovery and presentation, then hand responsibility back to the system that already manages the transaction.
A working prototype answers better questions
The value of doing this outside the final stack becomes clearer once you start using it.
Before anyone builds templates, configures CMS fields, or works through deployment requirements, you can answer real questions:
- Which Greenhouse fields are actually present, and how consistent are they?
- Do users need a title search, a location filter, metadata filters, or some combination?
- What should a job-detail URL look like?
- Which filters should be reflected in the query string so a view can be shared?
- What happens when a job has missing metadata or a board returns no results?
- Where does your interface stop and Greenhouse begin?
Those aren’t necessarily difficult engineering questions. They’re product and integration questions. Getting them wrong can leave you with a technically correct build that doesn’t work particularly well for the people using it.
And if you discover a bad assumption in a standalone prototype, you can change it without also untangling the CMS, the theme, the framework, the deployment process, and everyone else’s expectations about the final build.
This doesn’t mean every project needs an MVP
I don’t think every feature should be prototyped separately. Some integrations are well understood. Some changes are too small to justify the extra step. Some applications depend so heavily on the surrounding system that an isolated version wouldn’t teach you very much.
A prototype also isn’t a substitute for production engineering. Accessibility, security review, error states, SEO, caching, monitoring, and operational requirements still need attention when something becomes a real product.
What matters is knowing which uncertainty you’re trying to remove.
If the hard part is infrastructure, test infrastructure. If the hard part is interaction, build the interaction. If the hard part is a third-party data source, pull the data and see what it actually gives you.
For this Greenhouse experiment, a little vanilla JavaScript was enough to take the guesswork out of the data and presentation model.
Build the right thing before you build it the right way
There’s a tendency to view anything outside the final architecture as throwaway work. I don’t buy that.
A small working model can become an executable description of how the integration should behave. You can inspect the data, show it to stakeholders, change the filters, test the application handoff, and carry those decisions into whatever platform ultimately hosts it.
The code may be reused, adapted, or discarded. The uncertainty it removes is the valuable part.
That’s why I rebuilt this job board without reaching for another framework or burying it inside a CMS first. Not because vanilla JavaScript is inherently better, and not because prototypes are always the answer.
Because sometimes the prototype is the thing that keeps the final implementation from being technically correct and experientially wrong.
Try the Greenhouse job board demo · View the source on GitHub