Streamlining Umbraco Headless Development: Automated Model Generation for Content Delivery API with Orval

Recently, I've been working on creating a Next.js version of Paul Seal's popular and excellent Clean Starter Kit package for Umbraco. A key challenge in any headless Umbraco setup is efficiently managing the content models and API clients. I want to share my latest journey in automating the generation of UI clients and models from Umbraco's Content Delivery API. The Starting Point: Content Delivery API Extensions For strongly typed models, I initially leveraged the excellent Umbraco.Community.DeliveryApiExtensions package. This is essentially like ModelsBuilder but specifically designed for headless implementations. I chose Hey API as my initial solution for model generation since it's used by Umbraco itself and had been recommended in several developer blogs I follow. Hey API is powerful and rapidly evolving, which is both a blessing and a curse - it offers many features but sometimes it's challenging to keep up with its pace of change. The Hey API Experience Having successfully used Hey API in previous projects, I expected a straightforward implementation. However, things had changed significantly since my last use. The new approach required separate client packages, so I installed @hey-api/client-fetch and ran the generator. While it produced output, the code wouldn't compile. After extensive debugging with help from Jon Whitter @ Cantarus, we discovered that a discriminator in the OpenAPI spec was causing model replication, leading to TypeScript compilation errors. I tried switching to the Next.js specific client with the same results. Looking at Umbraco's source code revealed they were using Hey API's legacy client mode with an internal implementation. This worked perfectly until I needed to implement Next.js revalidation, which requires additional properties in fetch commands. Unfortunately, the legacy client in Hey API is sealed and not extensible. Enter Orval: A Better Alternative While reviewing the DeliveryAPI Extensions documentation, I noticed a reference to Orval, a tool I hadn't encountered before. After installing and experimenting with it, I was impressed by how quickly it generated usable models right out of the box. Simple Configuration Orval's configuration is refreshingly straightforward: module.exports = { 'petstore-file': { input: './petstore.yaml', output: './src/petstore.ts', }, }; and more complex example would be module.exports = { "umbraco-api": { input: { target: 'https://localhost/umbraco/swagger/delivery/swagger.json', validation: false, }, output: { mode: 'split', target: './src/api/umbraco/', client: 'fetch' }, }, }; Flexible Features Orval offers several advantages: Multiple output modes (single file, tag-based split, etc.) Support for various HTTP clients (axios, fetch, etc.) Mock generation through MSW using OpenAPI examples Zod schema generation for validation Most importantly, it solved my Next.js integration issue. Orval provides a clean way to override their client with a custom implementation while still passing Next.js specific parameters through the generated client to the service layer. Here's my custom client implementation that enables Next.js revalidation: module.exports = { 'umbraco-api': { output: { mode: 'tags-split', target: './src/api/client.ts', baseUrl: 'http://localhost/', schemas: './src/api/model', client: 'fetch', override: { mutator: { path: './src/custom-fetch.ts', name: 'customFetch', }, }, }, input: { target: 'http://localhost/umbraco/swagger/delivery/swagger.json', }, } } and the custom fetch client const getBody = (c: Response | Request): Promise => { return c.json(); }; const getUrl = (contextUrl: string): string => { const url = new URL(contextUrl); const pathname = url.pathname; const search = url.search; const baseUrl = process.env.NEXT_PUBLIC_UMBRACO_BASE_URL; const requestUrl = new URL(`${baseUrl}${pathname}${search}`); return requestUrl.toString(); }; const getHeaders = (headers?: HeadersInit): HeadersInit => { return { ...headers, 'Content-Type': 'application/json', }; }; export const customFetch = async ( url: string, options: RequestInit, ): Promise => { const requestUrl = getUrl(url); const requestHeaders = getHeaders(options.headers); const requestInit: RequestInit = { ...options, headers: requestHeaders }; const response = await fetch(requestUrl, requestInit); const data = await getBody(response); return { status: response.status, data, headers: response.headers } as T; }; Resulting client call const response = await getContent20({ fetch: "children:/", sort: ["sortOrder:asc"] }, { next: { tags: ['navigation']

Apr 10, 2025 - 11:28
 0
Streamlining Umbraco Headless Development: Automated Model Generation for Content Delivery API with Orval

Recently, I've been working on creating a Next.js version of Paul Seal's popular and excellent Clean Starter Kit package for Umbraco. A key challenge in any headless Umbraco setup is efficiently managing the content models and API clients. I want to share my latest journey in automating the generation of UI clients and models from Umbraco's Content Delivery API.

The Starting Point: Content Delivery API Extensions

For strongly typed models, I initially leveraged the excellent Umbraco.Community.DeliveryApiExtensions package. This is essentially like ModelsBuilder but specifically designed for headless implementations.

I chose Hey API as my initial solution for model generation since it's used by Umbraco itself and had been recommended in several developer blogs I follow. Hey API is powerful and rapidly evolving, which is both a blessing and a curse - it offers many features but sometimes it's challenging to keep up with its pace of change.

The Hey API Experience

Having successfully used Hey API in previous projects, I expected a straightforward implementation. However, things had changed significantly since my last use.

The new approach required separate client packages, so I installed @hey-api/client-fetch and ran the generator. While it produced output, the code wouldn't compile. After extensive debugging with help from Jon Whitter @ Cantarus, we discovered that a discriminator in the OpenAPI spec was causing model replication, leading to TypeScript compilation errors.

I tried switching to the Next.js specific client with the same results. Looking at Umbraco's source code revealed they were using Hey API's legacy client mode with an internal implementation. This worked perfectly until I needed to implement Next.js revalidation, which requires additional properties in fetch commands. Unfortunately, the legacy client in Hey API is sealed and not extensible.

Enter Orval: A Better Alternative

While reviewing the DeliveryAPI Extensions documentation, I noticed a reference to Orval, a tool I hadn't encountered before. After installing and experimenting with it, I was impressed by how quickly it generated usable models right out of the box.

Simple Configuration

Orval's configuration is refreshingly straightforward:

module.exports = {
   'petstore-file': {
     input: './petstore.yaml',
     output: './src/petstore.ts',
   },
};

and more complex example would be

module.exports = {
  "umbraco-api": {
    input: {
      target: 'https://localhost/umbraco/swagger/delivery/swagger.json',
      validation: false,
    },
    output: {
      mode: 'split',
      target: './src/api/umbraco/',
      client: 'fetch'
    },
  },
};

Flexible Features

Orval offers several advantages:

  • Multiple output modes (single file, tag-based split, etc.)
  • Support for various HTTP clients (axios, fetch, etc.)
  • Mock generation through MSW using OpenAPI examples
  • Zod schema generation for validation

Most importantly, it solved my Next.js integration issue. Orval provides a clean way to override their client with a custom implementation while still passing Next.js specific parameters through the generated client to the service layer.

Here's my custom client implementation that enables Next.js revalidation:

module.exports = {
    'umbraco-api': {
      output: {
        mode: 'tags-split',
        target: './src/api/client.ts',
        baseUrl: 'http://localhost/',
        schemas: './src/api/model',
        client: 'fetch',
        override: {
            mutator: {
                path: './src/custom-fetch.ts',
                name: 'customFetch',
            },
        },
      },
      input: {
        target: 'http://localhost/umbraco/swagger/delivery/swagger.json',
      },
    }
}

and the custom fetch client

const getBody = (c: Response | Request): Promise => {
    return c.json();
};

const getUrl = (contextUrl: string): string => {

  const url = new URL(contextUrl);
  const pathname = url.pathname;
  const search = url.search;
  const baseUrl = process.env.NEXT_PUBLIC_UMBRACO_BASE_URL;

  const requestUrl = new URL(`${baseUrl}${pathname}${search}`);

  return requestUrl.toString();
};

const getHeaders = (headers?: HeadersInit): HeadersInit => {
  return {
    ...headers,
    'Content-Type': 'application/json',
  };
};

export const customFetch = async (       
  url: string,
  options: RequestInit,
): Promise => {
  const requestUrl = getUrl(url);
  const requestHeaders = getHeaders(options.headers);

  const requestInit: RequestInit = {
    ...options,
    headers: requestHeaders
  };

  const response = await fetch(requestUrl, requestInit);
  const data = await getBody(response);

  return { status: response.status, data, headers: response.headers } as T;
};

Resulting client call

    const response = await getContent20({
        fetch: "children:/",
        sort: ["sortOrder:asc"]
    }, {
      next: {
        tags: ['navigation'],
      }
    });

This approach allowed me to set Next.js-specific properties at the service layer, giving me optimal control over caching and revalidation.

Multiple API Integration

As a bonus, Orval also allows you to add several client generators at once. This means you can generate clients for multiple APIs in a single configuration:

module.exports = {
  "content-delivery-api": {
    input: {
      target: 'http://localhost/umbraco/swagger/delivery/swagger.json',
      validation: false,
    },
    output: {
      mode: 'split',
      target: './src/api/content/',
      client: 'fetch'
    },
  },
  "commerce-delivery-api": {
    input: {
      target: 'http://localhost/umbraco/swagger/commerce/swagger.json',
      validation: false,
    },
    output: {
      mode: 'split',
      target: './src/api/commerce/',
      client: 'fetch'
    },
  },
  "custom-api": {
    input: {
      target: 'http://localhost/umbraco/swagger/my-api/swagger.json',
      validation: false,
    },
    output: {
      mode: 'tags',
      target: './src/api/custom/',
      client: 'fetch'
    },
  }
};

This is particularly powerful when working with Umbraco sites that have multiple APIs - you can generate clients for the Content Delivery API, Commerce Delivery API, and even your own custom APIs if you implement Swagger on them (which is straightforward to do in Umbraco). Everything stays strongly typed with minimal effort.

Why Orval Wins

After comparing both solutions, I now recommend Orval over other options for generating TypeScript clients and models from Umbraco's Content Delivery API. Here's why:

  1. Simplicity: It includes only what's needed without bloat
  2. Stability: Like Umbraco itself, Orval prioritizes reliability over rapid feature addition
  3. Flexibility: The override system makes integration with frameworks like Next.js much cleaner
  4. Documentation: Clear, concise documentation makes implementation straightforward

While I admire Hey API's ambition, its rapid evolution creates instability when working with Umbraco's Content Delivery API. For production projects, I prefer Orval's more focused, stable approach.

Conclusion

Building a Next.js frontend for Umbraco using the Content Delivery API becomes much more manageable with the right tooling. The combination of Umbraco's Content Delivery API Umbraco.Community.DeliveryApiExtensions and Orval creates a developer experience that's both powerful and maintainable.

I'd love to hear your experiences with headless Umbraco setups and the tools you're using to streamline your workflow. Have you tried Orval or Hey API? What's working best for your projects?