How to Hook Modern JavaScript Frameworks into Sling's REST API

You’ve probably felt the sting of waiting for a back‑end change to show up in your UI. In 2024 the pace of front‑end innovation is relentless, yet many Sling projects still rely on server‑side JSPs that make every tweak feel like pulling teeth. Connecting a modern JavaScript framework directly to Sling’s REST API can shave hours off your dev cycle and keep the UI fresh without the old page‑reload dance.

Why the REST API Is Your New Best Friend

Sling’s REST API is essentially a set of URLs that map straight to the JCR (Java Content Repository). Every node, every property, every binary can be fetched with a simple GET request. The beauty is that the API is already there – you don’t need to write a custom servlet just to expose data.

When you treat those URLs as “headless” endpoints, you get two big wins:

  1. Speed – No server‑side rendering, just JSON over HTTP. Your front‑end can render instantly once the data lands.
  2. Flexibility – Swap React for Vue, or Svelte for Solid, without touching the back‑end. The API stays the same.

That’s why I started pulling data into a React app for a client’s product catalog last summer. Within a week we had a live, searchable UI that updated in real time, while the Sling team kept their content workflow untouched.

Pick Your Front‑End Weapon

Before you start wiring code, decide which framework feels comfortable for the team. Here’s a quick cheat sheet:

FrameworkLearning curveBundle sizeCommunity
ReactMediumModerateHuge
Vue 3LowSmallGrowing
SvelteLowTinyNiche

All three can talk to Sling with the same fetch logic, so pick the one that matches your project’s rhythm. I’ll use React in the walkthrough because it’s the most common in my circle, but the same steps apply to Vue or Svelte with minor syntax tweaks.

Step‑by‑Step: Wiring React to Sling

1. Set Up a Minimal React Project

npx create-react-app sling‑client
cd sling‑client
npm start

That gives you a dev server on http://localhost:3000. Keep it running while you experiment.

2. Define the API Base URL

Create a file src/config.js:

export const API_ROOT = 'https://author.example.com/content';

Replace the domain with your Sling author instance. If you need authentication, you can add a token header later.

3. Write a Tiny Fetch Wrapper

In src/api.js:

import { API_ROOT } from './config';

export async function fetchNode(path) {
  const url = `${API_ROOT}${path}.json`;
  const response = await fetch(url, {
    credentials: 'include' // sends cookies for auth if needed
  });
  if (!response.ok) {
    throw new Error(`Failed to fetch ${path}: ${response.status}`);
  }
  return response.json();
}

Sling automatically adds a .json selector to any node, turning the JCR node into a plain JSON object. No extra servlet needed.

4. Pull Data into a Component

import React, { useEffect, useState } from 'react';
import { fetchNode } from './api';

function ProductList() {
  const [products, setProducts] = useState([]);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchNode('/products')
      .then(data => {
        // Sling returns children under `:children`
        const items = data[':children'] || [];
        setProducts(items);
      })
      .catch(err => setError(err.message));
  }, []);

  if (error) return <div>Error: {error}</div>;
  if (!products.length) return <div>Loading…</div>;

  return (
    <ul>
      {products.map(p => (
        <li key={p[':name']}>
          <h3>{p.title}</h3>
          <p>{p.description}</p>
        </li>
      ))}
    </ul>
  );
}

export default ProductList;

A couple of notes:

  • Sling’s JSON format nests child nodes under :children. The :name property holds the node name.
  • If you store fields like title or description as JCR properties, they appear as plain keys.

5. Handle Writes with POST / PUT

For creating or updating content, Sling accepts POST with a :operation=import selector, but a simpler route is to use the POST to the node’s URL with form data. Here’s a quick example for adding a new product:

export async function createProduct(parentPath, payload) {
  const url = `${API_ROOT}${parentPath}`;
  const form = new URLSearchParams();
  Object.entries(payload).forEach(([k, v]) => form.append(k, v));

  const response = await fetch(url, {
    method: 'POST',
    body: form,
    credentials: 'include',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded'
    }
  });

  if (!response.ok) {
    throw new Error(`Create failed: ${response.status}`);
  }
  return response.json();
}

You can call this from a form component, and Sling will create a new child node under parentPath. The node name will be generated automatically unless you pass :name=your‑slug in the payload.

6. Secure the Calls

If your author instance sits behind a firewall, you’ll likely need a token. Sling can issue OAuth tokens or simple JWTs via the login servlet. Store the token in a secure http‑only cookie or in memory, and add an Authorization: Bearer <token> header in the fetch wrapper.

const token = localStorage.getItem('slingToken');
const response = await fetch(url, {
  headers: { Authorization: `Bearer ${token}` },
  credentials: 'include'
});

Never expose the token in the URL; it leaks to logs.

Common Pitfalls and How to Dodge Them

CORS Errors

Sling’s default configuration blocks cross‑origin requests. Add a simple OSGi config to enable CORS for your dev domain:

# in org.apache.sling.commons.osgi:org.apache.sling.commons.osgi
sling.cors.allowedOrigins = http://localhost:3000
sling.cors.allowedMethods = GET,POST,PUT,DELETE

Restart the bundle and the browser will stop complaining.

Missing :children Wrapper

If you request a node that has no child nodes, Sling returns the properties directly, without a :children array. Guard against this by checking both cases:

const items = data[':children'] || Object.keys(data).filter(k => !k.startsWith(':')).map(k => data[k]);

Over‑Fetching Large Trees

A deep JCR tree can balloon the JSON payload. Use Sling’s depth selector (.json/0.1) to limit recursion, or request only the needed properties with ?property=title,description.

const url = `${API_ROOT}${path}.json?depth=1&property=title,description`;

Wrap‑Up: Faster Development, Cleaner Code

By treating Sling as a pure JSON API, you unlock the same rapid iteration that modern front‑end frameworks promise. No more juggling JSPs, no more waiting for a back‑end build to finish before you can see a UI change. The pattern is simple:

  1. Expose nodes with .json.
  2. Pull them into React/Vue/Svelte with a tiny fetch wrapper.
  3. Write back with standard POST or PUT.
  4. Tweak CORS and auth once, then let the front‑end run free.

I’ve seen teams cut their release cadence from monthly to weekly just by making this switch. If you’re still stuck in the JSP era, give the API a spin – the learning curve is shallow, the payoff is steep, and your future self will thank you.

Reactions