API Routes

Greenwood has support for API routes, which are just functions that run on the server, and take in a Request and return a Response. Each API route must export an async function called handler.

Usage

API routes follow a file-based routing convention, within the pages directory.

So this structure will yield an endpoint available at /api/greeting in the browser:

src/
  pages/
    api/
      greeting.js

Here is an example of that API Route, which reads a query parameter of name and returns a JSON response:

// src/pages/api/greeting.js
export async function handler(request) {
  const params = new URLSearchParams(request.url.slice(request.url.indexOf("?")));
  const name = params.has("name") ? params.get("name") : "World";
  const body = { message: `Hello ${name}! 👋` };

  return new Response(JSON.stringify(body), {
    headers: new Headers({
      "Content-Type": "application/json",
    }),
  });
}

Hypermedia

Inspired by Doug Parker's blog post A Simpler HTML-over-the-Wire and tools like htmx, one useful pattern afforded by Greenwood is the ability to render the same custom element definition on the client and the server. This "fragments" API approach can be used to server render Web Component definitions, such that as the HTML is added to the DOM from the response, these components will hydrate automatically and become instantly interactive if the same definition has also been loaded on the client via a <script> tag. (think of appending more items to a search results page or virtualized list.)

An example of rendering a "card" component in an API Route might look like look this:

// src/component/card.js
export default class Card extends HTMLElement {
  connectedCallback() {
    if (!this.shadowRoot) {
      const title = this.getAttribute("title");
      const thumbnail = this.getAttribute("thumbnail");
      const template = document.createElement("template");

      template.innerHTML = `
        <style>
          /* ... */
        </style>
        <div>
          <h3>${title}</h3>
          <img src="${thumbnail}" alt="${title}" loading="lazy">
        </div>
      `;

      this.attachShadow({ mode: "open" });
      this.shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

customElements.define("x-card", Card);

And here is it being used in an API Route handler:

// src/pages/api/search.js
import { renderFromHTML } from "wc-compiler";
import { getProducts } from "../../db/products.js";

export async function handler(request) {
  // use the web standard FormData to get the incoming form submission
  const formData = await request.formData();
  const term = formData.has("term") ? formData.get("term") : "";
  const products = await getProducts(term);
  const { html } = await renderFromHTML(
    `
    ${products
      .map((item) => {
        const { title, thumbnail } = item;

        return `
          <app-card
            title="${title}"
            thumbnail="${thumbnail}"
          ></app-card>
        `;
      })
      .join("")}
  `,
    [new URL("../../components/card.js", import.meta.url)],
  );

  return new Response(html, {
    headers: new Headers({
      "Content-Type": "text/html",
    }),
  });
}

To learn more about this pattern, checkout our Full-Stack Web Components tutorial for a more complete example.

Isolation Mode

To execute an API route in its own isolated rendering context, you can export an isolation option from your page, set to true.

export const isolation = true;

For more information and how you can enable this for all pages, please see the isolation configuration docs.