How to sort Webflow CMS items by any field in a Webflow Cloud Route Handler

How to sort Webflow CMS items by any field in a Webflow Cloud Route Handler

Sort Webflow CMS items by custom fields with the Data API sort parameter, and sort option fields in a Webflow Cloud Route Handler.

How to sort Webflow CMS items by any field in a Webflow Cloud Route Handler

Ismail Ajagbe
Technical Author
View author profile
Ismail Ajagbe
Technical Author
View author profile
Table of contents

Let the Webflow Data API sort the fields it can, and sort the rest in a Webflow Cloud Route Handler.

A list of events or products hardly stays in the order the CMS returns it. The Data API now sorts plain text, number, date and switch fields for you with sort[field]=desc, so most ordered lists need one request. Option and reference fields return IDs, though, and those need a comparator in your own code.

In this guide, we’ll build a Next.js Route Handler on Webflow Cloud that asks the API to sort where it can and sorts in the route where it can't. The token stays on the server, and the page receives an ordered list.

What do you need to sort Collection items from the Data API in Webflow?

One thing you should note: alphabetical sorting in the Designer compares text, so "10" sorts before "7". The route in this tutorial is for lists your app renders outside the Designer.

For the route you need:

  • A Collection with published items: Starter's limited CMS is enough to follow along, and Premium covers the full CMS and mounting the app on a custom domain.
  • A site token: With the cms:read scope. On a client site, the site owner may need to create it for you.
  • Node.js 22 and npm: Webflow Cloud supports only the npm package manager, so install commands use npm.
  • Next.js 15: Pin create-next-app@15 so the scaffold stays on Next.js 15, the release line Webflow Cloud supports.
  • The field slugs to sort by: event-date, price or category, from each field's settings in the CMS.

If the list lives on a Webflow page, you may not need code at all. A Collection List sorts in the Designer under its sort settings, with stacked rules per field, and the guide to dynamic sorting shows visitor-controlled sorting on a page.

5 steps to sort Collection items from the Data API in Webflow Cloud

The build is a fetch helper that asks the API for sorted pages, a comparator for option fields, a Route Handler with an allowlist of sorts, and a client component that calls it.

1. Store the API token and create the app

Scaffold the app and keep the token out of the repository. Run this from the folder where you keep projects:

npx create-next-app@15 sorted-items --ts --app --eslint --no-tailwind --no-src-dir --import-alias "@/*" --use-npm --no-turbopack
cd sorted-items

Add WEBFLOW_API_TOKEN and WEBFLOW_COLLECTION_ID to .env.local, and the same two to your Webflow Cloud environment, with the token marked secret. Webflow's environment variables page says both kinds are available to the build and to the deployed app at runtime, and secret values are redacted from build logs.

Leave export const runtime = 'edge' out of every file, even though the bring-your-own-app page still suggests it for API routes. Webflow Cloud runs on Cloudflare Workers, but the Next.js edge runtime target is a separate setting that the OpenNext Cloudflare adapter does not support, so the directive breaks the build.

2. Ask the Data API to sort the fields it supports

The List Live Items endpoint takes sort[<fieldSlug>]=asc or desc for plain text, email, phone, number, date and switch fields, up to three fields per request, applied in the order you list them.

For built-in fields, sortBy accepts createdOn, lastPublished, lastUpdated, name and slug. A field of another type, or a slug that does not exist, returns a 400.

The helper calls the same endpoint on the Content Delivery API host, api-cdn.webflow.com, which serves cached responses that don't count against the Data API rate limit.

Webflow API helper

Create lib/webflow.ts:

// lib/webflow.ts
const CDN = 'https://api-cdn.webflow.com/v2';
const API = 'https://api.webflow.com/v2';

export type WebflowItem = { id: string; fieldData: Record<string, unknown> };

async function get<T>(url: string): Promise<T> {
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.WEBFLOW_API_TOKEN}`, accept: 'application/json' },
  });
  if (!res.ok) throw new Error(`Webflow API ${res.status}: ${await res.text()}`);
  return (await res.json()) as T;
}

type Page = { items: WebflowItem[]; pagination: { total: number } };

// One page of live items, sorted by the API when `params` carries sort[...] keys.
export async function listLive(collectionId: string, params: URLSearchParams): Promise<WebflowItem[]> {
  const page = await get<Page>(`${CDN}/collections/${collectionId}/items/live?${params}`);
  return page.items;
}

// Every live item, for sorts the API cannot do.
export async function listAllLive(collectionId: string): Promise<WebflowItem[]> {
  const all: WebflowItem[] = [];
  let offset = 0;
  for (;;) {
    const params = new URLSearchParams({ limit: '100', offset: String(offset) });
    const page = await get<Page>(`${CDN}/collections/${collectionId}/items/live?${params}`);
    all.push(...page.items);
    offset += page.items.length;
    if (page.items.length === 0 || offset >= page.pagination.total) return all;
  }
}

type Field = { slug: string; validations?: { options?: { id: string; name: string }[] } };

// Maps each option ID of an Option field to its display name.
export async function optionNames(collectionId: string, slug: string): Promise<Map<string, string>> {
  const collection = await get<{ fields: Field[] }>(`${API}/collections/${collectionId}`);
  const field = collection.fields.find((f) => f.slug === slug);
  return new Map((field?.validations?.options ?? []).map((o) => [o.id, o.name]));
}

For an API sort, one request returns the first 100 items already in order, which covers a "newest 20" list or a full catalog page without collecting every item first.

3. Write a comparator for the fields the API can't sort

An Option field stores an option ID, and a Reference field stores the referenced item's ID, so sorting the raw values would order the list by ID. The fix is to swap each ID for its display name before comparing. Option names come from the collection schema that optionNames reads; a Reference field works the same way, using the referenced collection's item names.

Text comparison uses Intl.Collator with numeric matching, so "Level 2" sorts before "Level 10". Items with no value go last in either direction, so a blank category never jumps to the top of the list.

Comparator implementation

Create lib/sort.ts:

// lib/sort.ts
import type { WebflowItem } from './webflow';

export type SortOrder = 'asc' | 'desc';

const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true });

export function sortByName(
  items: WebflowItem[],
  field: string,
  names: Map<string, string>,
  order: SortOrder,
): WebflowItem[] {
  const direction = order === 'desc' ? -1 : 1;
  const label = (item: WebflowItem) => {
    const id = item.fieldData[field];
    return typeof id === 'string' ? (names.get(id) ?? null) : null;
  };

  return [...items].sort((a, b) => {
    const av = label(a);
    const bv = label(b);
    if (av === null && bv === null) return 0;
    if (av === null) return 1;
    if (bv === null) return -1;
    return collator.compare(av, bv) * direction;
  });
}

The function copies the array before sorting, and Array.prototype.sort is stable, so items with the same option keep the order the API returned them in. For a localized site, pass the locale's language to Intl.Collator along with the matching cmsLocaleId on the request.

4. Expose the sorted list from a Route Handler

The route accepts a sort key from the query string and looks it up in an allowlist, so a visitor can only request sorts you defined. API sorts send sort[...] to Webflow; the option sort collects every item and uses the comparator.

Create app/api/items/route.ts:

// app/api/items/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { listAllLive, listLive, optionNames } from '@/lib/webflow';
import { sortByName, type SortOrder } from '@/lib/sort';

const SORTS: Record<string, { field: string; order: SortOrder; local?: 'option' }> = {
  newest: { field: 'event-date', order: 'desc' },
  price: { field: 'price', order: 'asc' },
  title: { field: 'name', order: 'asc' },
  category: { field: 'category', order: 'asc', local: 'option' },
};

export async function GET(request: NextRequest) {
  const collectionId = process.env.WEBFLOW_COLLECTION_ID;
  if (!collectionId || !process.env.WEBFLOW_API_TOKEN) {
    return NextResponse.json({ error: 'Route is not configured' }, { status: 500 });
  }

  const key = request.nextUrl.searchParams.get('sort') ?? 'newest';
  const sort = SORTS[key];
  if (!sort) {
    return NextResponse.json({ error: `Unknown sort: ${key}` }, { status: 400 });
  }

  try {
    let items;
    if (sort.local === 'option') {
      const [all, names] = await Promise.all([
        listAllLive(collectionId),
        optionNames(collectionId, sort.field),
      ]);
      items = sortByName(all, sort.field, names, sort.order);
    } else {
      const params = new URLSearchParams({ limit: '100' });
      params.set(`sort[${sort.field}]`, sort.order);
      items = await listLive(collectionId, params);
    }
    return NextResponse.json({
      count: items.length,
      items: items.map(({ id, fieldData }) => ({ id, fieldData })),
    });
  } catch (error) {
    console.error(error);
    return NextResponse.json({ error: 'Upstream request failed' }, { status: 502 });
  }
}

Change the slugs in SORTS to match your Collection. The allowlist also keeps arbitrary slugs out of the upstream request, so a visitor cannot trigger a 400 by guessing field names. The route is public, which suits published content; add authentication if the list is not public.

Run npm run dev and request /api/items?sort=price: the JSON should list items cheapest first, and ?sort=category should group them by option name rather than by ID.

5. Call the route from the client with the base path

A client component lets the visitor switch sorts. Prefix its fetch with the mount path, because Webflow Cloud does not rewrite client-side fetch calls; add a NEXT_PUBLIC_BASE_PATH variable set to the mount path, since Webflow Cloud does not create it.

Create app/components/SortedList.tsx:

// app/components/SortedList.tsx
'use client';

import { useEffect, useState } from 'react';

type Item = { id: string; fieldData: { name?: string } };

const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '';

export default function SortedList() {
  const [sort, setSort] = useState('newest');
  const [items, setItems] = useState<Item[]>([]);
  const [error, setError] = useState(false);

  useEffect(() => {
    let ignore = false;
    fetch(`${basePath}/api/items?sort=${encodeURIComponent(sort)}`)
      .then((res) => {
        if (!res.ok) throw new Error(String(res.status));
        return res.json();
      })
      .then((data: { items: Item[] }) => {
        if (!ignore) {
          setItems(data.items);
          setError(false);
        }
      })
      .catch(() => {
        if (!ignore) setError(true);
      });
    return () => {
      ignore = true;
    };
  }, [sort]);

  return (
    <section>
      <select value={sort} onChange={(e) => setSort(e.target.value)}>
        <option value="newest">Newest first</option>
        <option value="price">Price, low to high</option>
        <option value="title">Title</option>
        <option value="category">Category</option>
      </select>
      {error && <p>Could not load the list.</p>}
      <ul>
        {items.map((item) => (
          <li key={item.id}>{item.fieldData.name ?? item.id}</li>
        ))}
      </ul>
    </section>
  );
}

A select fires once per change, so the component needs no debounce. The ignore flag stops a slow response for an earlier sort from overwriting the list after the visitor has already picked another one.

Render the component from app/page.tsx, deploy, and switch sorts on the mounted page; the network panel should show each request going to the mount path.

What causes Data API sorting to fail in Webflow Cloud?

Most failures come from asking the API to sort on a field it doesn't support, or from Webflow Cloud's build and routing rules.

The route returns 502, and the log shows a 400 BadArgument

Cause: The sort[...] key names a field the API cannot sort, such as an Option or Reference field, or a slug that does not exist in the Collection. The API rejects the whole request rather than ignoring the key.

Fix: Check the field's type in the Collection settings. Plain text, email, phone, number, date and switch fields sort through the API; move anything else to a local entry in SORTS, and correct the slug if it differs from the field's settings.

The build fails, or the list returns 404 only after deploy

Cause: A build failure usually means a file carries export const runtime = 'edge', which the OpenNext Cloudflare adapter does not support. A 404 on the deployed page means the client fetch went to /api/items at the site root instead of under the mount path.

Fix: Delete the runtime directive from every file and redeploy. For the 404, set NEXT_PUBLIC_BASE_PATH to the exact mount path and redeploy, since Next.js inlines public values when it builds.

The route slows down or hits 429 once real traffic arrives

Cause: The option sort collects every item on every request. On a large Collection that is many requests per page view, and if the helper points at api.webflow.com instead of the delivery host, each one counts against the Data API rate limit.

Fix: Keep the helper on api-cdn.webflow.com, where cached responses cost nothing against the limit, and prefer API sorts wherever the field type allows. A Cache-Control header on the route won't help, because Webflow Cloud replaces it with private, no-cache; the 429 errors guide covers a server-side cache instead.

Items come back in an order that looks almost right

Cause: A number or date stored in a Plain text field sorts as text, so "9.5" lands above "10" and dates in mixed formats drift. Recent edits can also be missing because the delivery host caches responses for up to five minutes.

Fix: Store numbers in a Number field and dates in a Date field, so the API sorts them by value. For freshly published edits, wait out the cache window before treating the order as wrong.

What you can build next with the Data API and Webflow

The same endpoint also filters with filter[<fieldSlug>][<operator>], so the next step is a filtered archive, such as upcoming events only, sorted by date, in a single request.

For catalogs that combine two Collections, fetch both, swap reference IDs for names, and merge them with the comparator.

For every list parameter and the field types each one accepts, Webflow's developer docs document the item value formats.

Frequently asked questions

Can I sort by two fields at once?

Yes. Add a second sort[...] key to the request, such as sort[event-date]=desc&sort[name]=asc; the API applies them in the order they appear, up to three fields. For an option field as one of the keys, sort locally and compare the second field when the first returns 0.

Does sorting in a Route Handler count against my Webflow Cloud limits?

Each visitor request runs the route, so it counts toward your app's request allowance. The calls the route makes to Webflow are subrequests, which don't count toward that total, but they do count against the Data API rate limit unless the delivery host serves them from cache.

Can I cache the sorted result to a file with Node's fs module?

No. Webflow Cloud's Workers runtime has no file system access, as its Node.js compatibility page notes, so file system calls fail. Rely on the delivery host's cache, or a Key Value Store binding for anything you compute yourself.


Last Updated
October 24, 2026
Category

Related articles


verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo
verifone logomonday.com logospotify logoted logogreenhouse logoclear logocheckout.com logoreddit logothe new york times logoideo logoupwork logodiscord logo

Lancez-vous gratuitement

Essayez Webflow aussi longtemps que vous le souhaitez avec notre forfait Starter gratuit. Souscrivez à un forfait Site payant pour publier, héberger et débloquer des fonctionnalités supplémentaires.

Get started — it’s free
Voir la démo

Essayez Webflow aussi longtemps que vous le souhaitez avec notre forfait Starter gratuit. Souscrivez à un forfait Site payant pour publier, héberger et débloquer des fonctionnalités supplémentaires.