JAVASCRIPT API Track · 5 Lessons

JavaScript API
Track

Query SSURGO directly from the browser. No server required. Build live tables, connect SDA data to MapLibre GL maps, and ship reusable soil data modules.

Lessons
01

fetch() to SDA

The browser-native way to call REST APIs

JavaScript

fetch() is built into every modern browser — no libraries needed. SDA accepts cross-origin requests, so this works directly from any webpage.

const SDA = 'https://sdmdataaccess.sc.egov.usda.gov/Tabular/post.rest';

async function querySDA(sql) {
  const body = new URLSearchParams({ query: sql, format: 'JSON+COLUMNNAME' });
  const resp = await fetch(SDA, { method: 'POST', body });
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
  const data = await resp.json();
  if (!data.Table || data.Table.length < 2) return { headers: [], rows: [] };
  const [headers, , ...rows] = data.Table;
  // Convert rows to objects with column names as keys
  return rows.map(row => Object.fromEntries(headers.map((h, i) => [h, row[i]])));
}

// Usage
const results = await querySDA(
  "SELECT TOP 5 musym, muname, muacres FROM mapunit ORDER BY muacres DESC"
);
console.table(results);
02

Render Results in HTML

Turn SDA JSON into a live dark table

JavaScript
async function renderTable(sql, containerId) {
  const el = document.getElementById(containerId);
  el.innerHTML = '<p style="color:#38bdf8">⟳ Loading…</p>';
  try {
    const body = new URLSearchParams({ query: sql, format: 'JSON+COLUMNNAME' });
    const { Table } = await fetch(SDA, { method: 'POST', body }).then(r => r.json());
    const [headers, , ...rows] = Table;

    el.innerHTML = `
      <p style="color:#22c55e">✓ ${rows.length} rows</p>
      <div style="overflow-x:auto">
        <table class="result-table">
          <thead><tr>
            ${headers.map(h => `<th>${h}</th>`).join('')}
          </tr></thead>
          <tbody>
            ${rows.slice(0, 50).map(r =>
              `<tr>${r.map(c => `<td>${c ?? '—'}</td>`).join('')}</tr>`
            ).join('')}
          </tbody>
        </table>
      </div>`;
  } catch(e) {
    el.innerHTML = `<p style="color:red">✗ ${e.message}</p>`;
  }
}

// Call it:
renderTable(
  "SELECT TOP 20 areasymbol, areaname FROM legend WHERE areasymbol LIKE 'WI%'",
  'my-container'
);
03

MapLibre GL + SDA Data

Color a soil map from live interpretation data

JavaScript
MapLibre GL JS is the open-source map library used by the Soil Intelligence platform. Include it with: <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>
// Query FSI ratings, then color a county fill layer
async function loadFSILayer(map, areasymbol) {
  const body = new URLSearchParams({
    query: `
      SELECT mu.musym, ci.interphrc AS fsi_class, ci.interphr AS fsi_score
      FROM legend l
      INNER JOIN mapunit mu  ON mu.lkey  = l.lkey
      INNER JOIN component c ON c.mukey  = mu.mukey
      INNER JOIN cointerp  ci ON ci.cokey = c.cokey
      WHERE l.areasymbol = '${areasymbol}'
        AND c.majcompflag = 'Yes'
        AND ci.mrulename  = 'Fragile Soil Index'
        AND ci.ruledepth  = 0
      ORDER BY c.comppct_r DESC
    `,
    format: 'JSON+COLUMNNAME'
  });
  const [headers, , ...rows] = (await fetch(SDA, {method:'POST',body}).then(r=>r.json())).Table;

  // Build lookup: musym → { fsi_class, fsi_score }
  const lookup = {};
  rows.forEach(row => {
    const obj = Object.fromEntries(headers.map((h,i) => [h, row[i]]));
    if (!lookup[obj.musym]) lookup[obj.musym] = obj;
  });

  console.log(`Loaded ${Object.keys(lookup).length} FSI ratings for ${areasymbol}`);
  return lookup;
}

function fsiToColor(cls) {
  const colors = {
    'Not fragile':        '#1b7837',
    'Slightly fragile':   '#78c679',
    'Moderately fragile': '#fecc5c',
    'Fragile':            '#fd8d3c',
    'Highly fragile':     '#f03b20',
    'Extremely fragile':  '#7b0007',
  };
  return colors[cls] || '#343450';
}
04

Reusable SDA Module

A drop-in client for any web project

JavaScript
/**
 * SDAClient — minimal, reusable SDA REST client
 * Drop into any web project. Zero dependencies.
 */
const SDAClient = (() => {
  const ENDPOINT = 'https://sdmdataaccess.sc.egov.usda.gov/Tabular/post.rest';
  const cache = new Map();

  async function query(sql, { useCache = true } = {}) {
    const key = sql.trim().toLowerCase();
    if (useCache && cache.has(key)) return cache.get(key);
    const body = new URLSearchParams({ query: sql, format: 'JSON+COLUMNNAME' });
    const resp = await fetch(ENDPOINT, { method: 'POST', body });
    if (!resp.ok) throw new Error(`SDA HTTP ${resp.status}`);
    const data = await resp.json();
    if (!data.Table || data.Table.length < 2) return [];
    const [headers, , ...rows] = data.Table;
    const result = rows.map(r => Object.fromEntries(headers.map((h,i) => [h, r[i]])));
    if (useCache) cache.set(key, result);
    return result;
  }

  async function dominantComponents(areasymbol) {
    return query(`
      SELECT mu.musym, mu.muname, mu.muacres,
             c.compname, c.comppct_r, c.drainagecl, c.hydgrp, c.taxclname
      FROM legend l
      INNER JOIN mapunit mu  ON mu.lkey  = l.lkey
      INNER JOIN component c ON c.mukey  = mu.mukey
      WHERE l.areasymbol = '${areasymbol}' AND c.majcompflag = 'Yes'
      ORDER BY mu.muacres DESC, c.comppct_r DESC
    `);
  }

  async function horizons(areasymbol) {
    return query(`
      SELECT mu.musym, c.compname, ch.hzname,
             ch.hzdept_r, ch.hzdepb_r, ch.om_r, ch.awc_r, ch.ph1to1h2o_r
      FROM legend l
      INNER JOIN mapunit mu  ON mu.lkey  = l.lkey
      INNER JOIN component c ON c.mukey  = mu.mukey
      INNER JOIN chorizon ch ON ch.cokey = c.cokey
      WHERE l.areasymbol = '${areasymbol}' AND c.majcompflag = 'Yes'
      ORDER BY mu.musym, c.comppct_r DESC, ch.hzdept_r
    `);
  }

  return { query, dominantComponents, horizons };
})();

// Usage
const comps = await SDAClient.dominantComponents('MN003');
console.log(`${comps.length} dominant components`);
This exact pattern (with the COMPANION_BASE constant) is what powers the Soil Atlas — all live soil profile queries use a version of this module.
05

Interactive Soil Profile Card

Animated horizon visualization from a live SDA query

JavaScript

This function renders an animated soil profile card — horizon bars colored by depth and OM, with AWC values — for any clicked map unit. This is the heart of the Soil Atlas detail panel.

async function renderSoilProfile(areasymbol, musym, container) {
  container.innerHTML = '<p style="color:#38bdf8">⟳ Loading profile…</p>';
  const body = new URLSearchParams({ format: 'JSON+COLUMNNAME', query: `
    SELECT c.compname, c.comppct_r, c.drainagecl,
           ch.hzname, ch.hzdept_r, ch.hzdepb_r,
           ch.om_r, ch.awc_r, ch.sandtotal_r, ch.claytotal_r
    FROM legend l
    INNER JOIN mapunit mu  ON mu.lkey  = l.lkey
    INNER JOIN component c ON c.mukey  = mu.mukey
    INNER JOIN chorizon ch ON ch.cokey = c.cokey
    WHERE l.areasymbol = '${areasymbol}'
      AND mu.musym     = '${musym}'
      AND c.majcompflag = 'Yes'
    ORDER BY c.comppct_r DESC, ch.hzdept_r
  `});

  const [headers, , ...rows] = (await fetch(SDA,{method:'POST',body}).then(r=>r.json())).Table;
  const data = rows.map(r => Object.fromEntries(headers.map((h,i)=>[h,r[i]])));
  if (!data.length) { container.innerHTML='No data'; return; }

  const maxDepth = Math.max(...data.map(d => +(d.hzdepb_r)||0));
  const hzColors = ['#2d1a06','#4a3018','#6b4a28','#8b6040','#a07848'];
  const comp = data[0];

  container.innerHTML = `
    <div style="font-family:monospace;font-size:12px;background:#0a0e1a;
                border-radius:8px;padding:16px">
      <div style="color:#c8a84b;font-weight:700;margin-bottom:8px">
        ${musym} · ${comp.compname} (${comp.comppct_r}%) · ${comp.drainagecl}
      </div>
      ${data.map((d, i) => {
        const thick = (+d.hzdepb_r - +d.hzdept_r)||0;
        const pct   = (thick / maxDepth * 100).toFixed(0);
        return `
          <div style="display:flex;align-items:center;gap:8px;margin-bottom:3px">
            <span style="width:56px;color:#5a6490;text-align:right;font-size:10px">
              ${d.hzdept_r}–${d.hzdepb_r} cm
            </span>
            <div style="flex:1;background:#181e30;border-radius:2px;height:22px;overflow:hidden">
              <div class="hz-anim" style="background:${hzColors[i%hzColors.length]};
                height:100%;width:0;display:flex;align-items:center;padding:0 8px;
                transition:width 1s ease" data-w="${pct}">
                <span style="color:rgba(255,255,255,.8);font-size:10px;white-space:nowrap">
                  ${d.hzname} · OM ${d.om_r||'—'}%
                </span>
              </div>
            </div>
            <span style="width:48px;color:#c8a84b;font-size:10px;text-align:right">
              ${d.awc_r ? d.awc_r+' AWC' : '—'}
            </span>
          </div>`;
      }).join('')}
    </div>`;
  // Animate bars
  setTimeout(() => container.querySelectorAll('.hz-anim').forEach(
    b => b.style.width = b.dataset.w + '%'
  ), 100);
}
Try It Live

Open the Query Lab

Run any SQL query live against SDA in a full-screen sandbox — with example queries, syntax highlighting, and CSV export.

Open Query Lab →