mxHeadless
REST API gateway for headless frontends on MODX 3. Resources, objects, OpenAPI, API keys, and OAuth

Nuxt 3/4 with mxHeadless: runtime config, composable, page by URI, and a server proxy.
nuxt.config.ts:
export default defineNuxtConfig({
runtimeConfig: {
mxheadlessApiKey: '',
public: {
mxheadlessBaseUrl: 'https://example.com/api/v1',
},
},
}).env:
NUXT_PUBLIC_MXHEADLESS_BASE_URL=https://example.com/api/v1
NUXT_MXHEADLESS_API_KEY=mxh_...Keep the API key on the server only (SSR, server/). Public reads often work without a key.
composables/useModxRest.ts:
type ModxRestEnvelope<T> = {
data: T
meta?: Record<string, unknown>
links?: Record<string, string>
}
export function useModxRest() {
const config = useRuntimeConfig()
const baseURL = config.public.mxheadlessBaseUrl as string
async function get<T>(path: string, query?: Record<string, string | number | boolean>) {
return $fetch<ModxRestEnvelope<T>>(path, {
baseURL,
query,
headers: import.meta.server && config.mxheadlessApiKey
? { Authorization: `Bearer ${config.mxheadlessApiKey}` }
: undefined,
})
}
return { get, baseURL }
}pages/[...slug].vue:
<script setup lang="ts">
const route = useRoute()
const uri = Array.isArray(route.params.slug)
? route.params.slug.join('/') + '.html'
: `${route.params.slug}.html`
const { get } = useModxRest()
const { data, error } = await useAsyncData(
`page-${uri}`,
() => get<Record<string, unknown>>(`/pages/${uri}`, {
fields: 'id,pagetitle,content,uri',
}),
)
if (error.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found' })
}
useSeoMeta({
title: () => String(data.value?.data?.pagetitle ?? ''),
})
</script>
<template>
<article v-if="data?.data">
<h1>{{ data.data.pagetitle }}</h1>
<!-- Sanitize HTML (DOMPurify) before rendering -->
<div>{{ data.data.content }}</div>
</article>
</template>Match the URI suffix (.html or /) to MODX friendly URLs.
server/api/news.get.ts keeps the key off the browser:
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
const query = getQuery(event)
return $fetch('/resources', {
baseURL: config.public.mxheadlessBaseUrl,
query: {
...query,
'filter[published]': 1,
},
headers: {
Authorization: `Bearer ${config.mxheadlessApiKey}`,
},
})
})Client: useFetch('/api/news').
const { get } = useModxRest()
const { data: products } = await useAsyncData('products', () =>
get('/objects/products', {
'filter[parent]': categoryId,
limit: 24,
sort: 'price',
fields: 'id,pagetitle,price,uri',
}),
)The key needs products.read. Configure CORS for a different origin.