{"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"CodeWalkthrough","attributes":{"filters":{},"filesets":[{"files":[{"path":"docs/realm/customization/api-functions/code-walkthrough-files/weather-markdoc/weather.ts","content":[{"start":0,"condition":{"steps":["api-imports"]},"children":["import type { ApiFunctionsContext } from '@redocly/config';"]},"",{"start":4,"condition":{"steps":["api-types"]},"children":["type WeatherApiError = {","  error?: {","    message?: string;","    code?: number;","  };","};","","type WeatherApiResponse = {","  location: {","    name: string;","    region: string;","    country: string;","    lat: number;","    lon: number;","    localtime: string;","  };","  current: {","    temp_c: number;","    temp_f: number;","    feelslike_c: number;","    feelslike_f: number;","    humidity: number;","    wind_kph: number;","    wind_mph: number;","    condition: {","      text: string;","      icon: string;","    };","  };","};"]},"",{"start":37,"condition":{"steps":["api-function"]},"children":["export default async function (request: Request, context: ApiFunctionsContext): Promise<Response> {"]},"",{"start":41,"condition":{"steps":["api-env"]},"children":["  const apiKey = process.env.WEATHER_API_KEY;","  if (!apiKey) {","    return context.status(500).json({","      error: 'Server configuration error',","      message: 'Weather API key is not configured',","    });","  }"]},"",{"start":51,"condition":{"steps":["api-params"]},"children":["  const queryLocation = context.query.location;","  if (queryLocation && typeof queryLocation !== 'string') {","    return context.status(400).json({","      error: 'Invalid location parameter',","      message: 'Please provide a single location',","    });","  }","","  const location =","    queryLocation || request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'auto:ip';"]},"",{"start":64,"condition":{"steps":["api-fetch"]},"children":["  try {","    const url = new URL('https://api.weatherapi.com/v1/current.json');","    url.searchParams.set('key', apiKey);","    url.searchParams.set('q', location);","    url.searchParams.set('aqi', 'no');","","    const weatherResponse = await fetch(url.toString());","","    if (!weatherResponse.ok) {","      const errorData: WeatherApiError = await weatherResponse.json();","      return context.status(weatherResponse.status).json({","        error: 'Weather API error',","        message: errorData.error?.message || 'Failed to fetch weather data',","        code: errorData.error?.code,","      });","    }","","    const weatherData: WeatherApiResponse = await weatherResponse.json();"]},"",{"start":85,"condition":{"steps":["api-response"]},"children":["    return context.status(200).json({","      location: weatherData.location,","      current: weatherData.current,","    });"]},{"start":91,"condition":{"steps":["api-fetch"]},"children":["  } catch (error: unknown) {","    console.error('Weather API error:', error);","    return context.status(500).json({ error: 'Internal server error' });","  }"]},"}",""],"metadata":{"steps":["api-imports","api-types","api-function","api-env","api-params","api-fetch","api-response","api-fetch"]},"basename":"weather.ts","language":"typescript"},{"path":"docs/realm/customization/api-functions/code-walkthrough-files/weather-markdoc/CurrentWeather.tsx","content":[{"start":0,"condition":{"steps":["component-import"]},"children":["import React, { useEffect, useState, ReactElement } from 'react';"]},"",{"start":4,"condition":{"steps":["component-types"]},"children":["type WeatherResponse = {","  location: {","    name: string;","    region: string;","    country: string;","    localtime: string;","  };","  current: {","    temp_c: number;","    temp_f: number;","    feelslike_c: number;","    feelslike_f: number;","    humidity: number;","    wind_kph: number;","    condition: {","      text: string;","      icon: string;","    };","  };","};","","type CurrentWeatherProps = {","  location?: string;","  units?: 'celsius' | 'fahrenheit';","};","","type WeatherState =","  | { status: 'loading' }","  | { status: 'error'; error: string }","  | { status: 'success'; weather: WeatherResponse };"]},"",{"start":37,"condition":{"steps":["component-function"]},"children":["export function CurrentWeather({ location, units = 'celsius' }: CurrentWeatherProps): ReactElement {","  const [state, setState] = useState<WeatherState>({ status: 'loading' });"]},"",{"start":42,"condition":{"steps":["component-fetch"]},"children":["  useEffect(() => {","    const abortController = new AbortController();","    const params = new URLSearchParams();","    if (location) {","      params.set('location', location);","    }","    const url = `/api/weather${params.toString() ? `?${params.toString()}` : ''}`;","","    async function getCurrentWeather(): Promise<void> {","      setState({ status: 'loading' });","","      try {","        const response = await fetch(url, {","          signal: abortController.signal,","        });","","        if (!response.ok) {","          setState({ status: 'error', error: `Request failed (${response.status})` });","          return;","        }","","        const weatherResponse: WeatherResponse = await response.json();","        setState({ status: 'success', weather: weatherResponse });","      } catch (e: unknown) {","        if (e instanceof DOMException && e.name === 'AbortError') {","          return;","        }","        setState({ status: 'error', error: e instanceof Error ? e.message : 'Request failed' });","      }","    }","","    getCurrentWeather();","    return () => abortController.abort();","  }, [location]);"]},"",{"start":79,"condition":{"steps":["component-render"]},"children":["  if (state.status === 'loading') return <div>Loading weather…</div>;","  if (state.status === 'error') return <div>Weather unavailable: {state.error}</div>;","","  const { weather } = state;","  const isFahrenheit = units === 'fahrenheit';","  const temperature = isFahrenheit ? `${weather.current.temp_f}°F` : `${weather.current.temp_c}°C`;","  const feelsLikeTemperature = isFahrenheit","    ? `${weather.current.feelslike_f}°F`","    : `${weather.current.feelslike_c}°C`;","","  return (","    <div>","      <strong>","        {weather.location.name}, {weather.location.country}","      </strong>","      <div>","        {weather.current.condition.text} · {temperature} (feels like {feelsLikeTemperature})","      </div>","      <div>","        Humidity: {weather.current.humidity}% · Wind: {weather.current.wind_kph} kph","      </div>","    </div>","  );"]},"}",""],"metadata":{"steps":["component-import","component-types","component-function","component-fetch","component-render"]},"basename":"CurrentWeather.tsx","language":"tsx"},{"path":"docs/realm/customization/api-functions/code-walkthrough-files/weather-markdoc/schema.ts","content":[{"start":0,"condition":{"steps":["tag-schema"]},"children":["import type { MarkdocTagSchema } from '@redocly/theme/markdoc/tags/types';","","export const tags: Record<string, MarkdocTagSchema> = {","  weather: {","    render: 'CurrentWeather',","    selfClosing: true,","    attributes: {","      location: {","        type: String,","        description: 'City to display weather for',","      },","      units: {","        type: String,","        default: 'celsius',","        matches: ['celsius', 'fahrenheit'],","        description: 'Temperature units',","      },","    },","  },","};"]},""],"metadata":{"steps":["tag-schema"]},"basename":"schema.ts","language":"typescript"},{"path":"docs/realm/customization/api-functions/code-walkthrough-files/weather-markdoc/components.tsx","content":[{"start":0,"condition":{"steps":["tag-export"]},"children":["export { CurrentWeather } from './components/CurrentWeather';"]},""],"metadata":{"steps":["tag-export"]},"basename":"components.tsx","language":"tsx"}],"downloadAssociatedFiles":[]}],"steps":[{"id":"prereqs","heading":"Prerequisites"},{"id":"api-imports","heading":"Import types"},{"id":"api-types","heading":"Define response types (optional)"},{"id":"api-function","heading":"Export the handler"},{"id":"api-env","heading":"Read the API key"},{"id":"api-params","heading":"Resolve the location"},{"id":"api-fetch","heading":"Fetch weather data"},{"id":"api-response","heading":"Return the response"},{"id":"component-import","heading":"Import React"},{"id":"component-types","heading":"Define component types"},{"id":"component-function","heading":"Create the component"},{"id":"component-fetch","heading":"Fetch from the API function"},{"id":"component-render","heading":"Render weather data"},{"id":"tag-schema","heading":"Add the tag schema"},{"id":"tag-export","heading":"Export the component"}],"inputs":{},"toggles":{}},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"api-functions-tutorial-render-weather-data-in-a-markdoc-tag","__idx":0},"children":["API functions tutorial: render weather data in a Markdoc tag"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Build a custom Markdoc tag that renders live weather data by calling an API function."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This tutorial explains how to create:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["an API function endpoint at ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/weather"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["a React component that fetches from that endpoint"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["a Markdoc tag that authors can use in Markdown files"]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"key-concepts","__idx":1},"children":["Key concepts"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API functions"]}," are server-side endpoints defined by adding TypeScript or JavaScript files inside the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@api"]}," folder."," ","The filename determines the URL path and, optionally, the HTTP method: ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["<name>.<method>.ts"]}," (for example, ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["weather.get.ts"]}," maps to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /api/weather"]},")."," ","Omit the method segment to handle all HTTP methods with a single file."," ","See ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/api-functions/api-functions-reference#file-system-and-method-routing"},"children":["File-system and method routing"]}," for the full naming reference."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Markdoc tags"]}," are custom components registered in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc"]}," folder."," ","You create a React component in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/components/"]},", export it from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/components.tsx"]},", and register its tag schema in ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/schema.ts"]},"."," ","See ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/build-markdoc-tags"},"children":["Build Markdoc tags"]}," for full details."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["In the following solution:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API key stays server-side"]}," in the API function (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["process.env.WEATHER_API_KEY"]},")."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["The Markdoc component renders live data by calling your own endpoint."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["You can apply role-based access control to the API function."," ","See ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/api-functions/api-functions-reference"},"children":["API functions reference"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"before-you-begin","__idx":2},"children":["Before you begin"]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"prereqs","heading":"Prerequisites"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Make sure you have the following:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":["familiarity with ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/build-markdoc-tags"},"children":["building Markdoc tags"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["understanding of ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/api-functions/api-functions-reference"},"children":["API function basics"]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":["a free API key from ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"https://www.weatherapi.com/","target":"_blank","rel":"noopener noreferrer"},"children":["weatherapi.com"]}," exposed as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["WEATHER_API_KEY"]}," in your environment variables"]}]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"create-the-weather-api-function","__idx":3},"children":["Create the Weather API function"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create the file ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@api/weather.ts"]},"."," ","This file defines an endpoint at ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/weather"]}," that accepts an optional query parameter ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]},"."," ","When ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," is omitted, the function falls back to the client's IP address for geolocation."]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-imports","heading":"Import types"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Import the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["ApiFunctionsContext"]}," type from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@redocly/config"]},"."," ","This type provides TypeScript definitions for the context object passed to every API function."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-types","heading":"Define response types (optional)"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Define types for the external weather API responses."," ","Typed responses improve editor support and catch integration errors early."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-function","heading":"Export the handler"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Export a default async function that receives a standard Web API ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"https://developer.mozilla.org/en-US/docs/Web/API/Request","target":"_blank","rel":"noopener noreferrer"},"children":["Request"]}," and the Redocly ",{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/api-functions/api-functions-reference#context","target":"_blank","rel":"noopener noreferrer"},"children":["context"]},"."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-env","heading":"Read the API key"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Access the API key from environment variables using ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["process.env"]},"."," ","Return a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["500"]}," error early if the key is missing so the caller gets a clear message instead of a cryptic upstream failure."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-params","heading":"Resolve the location"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," query parameter if the caller provides one."," ","Otherwise, fall back to the client IP address from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["x-forwarded-for"]}," (or ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["auto:ip"]}," as a last resort) so the weather API geolocates the visitor automatically."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-fetch","heading":"Fetch weather data"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Construct the URL for the external weather API and map your variables to the parameters required by the provider."," ","For example, map your ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," variable to their ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["q"]}," parameter, and set ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["aqi"]}," to ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["no"]}," to exclude Air Quality Index data."," ","Call the external weather API with ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["fetch"]},", and handle non-OK responses by returning the upstream error details."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"api-response","heading":"Return the response"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Return the relevant subset of the weather data as JSON."," ","The Markdoc component will consume this shape."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"create-the-markdoc-component","__idx":4},"children":["Create the Markdoc component"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Create the file ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/components/CurrentWeather.tsx"]},"."," ","This React component fetches from your API function and renders the result."]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"component-import","heading":"Import React"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Import React so you can use hooks and JSX."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"component-types","heading":"Define component types"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Define the expected API response shape and component props."," ","The optional ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," prop lets authors specify a city; ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["units"]}," chooses Celsius or Fahrenheit."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"component-function","heading":"Create the component"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Declare the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CurrentWeather"]}," function component with a state machine that tracks loading, error, and success states."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"component-fetch","heading":"Fetch from the API function"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Use ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["useEffect"]}," to call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["/api/weather"]}," when the component mounts."," ","If ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," is provided, pass it as a query parameter; otherwise omit it and let the API function resolve the location from the client IP."," ","An ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["AbortController"]}," cancels the request if the component unmounts before the response arrives."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"component-render","heading":"Render weather data"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Render loading and error states first, then display the location, temperature, humidity, and wind speed."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"register-the-markdoc-tag","__idx":5},"children":["Register the Markdoc tag"]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"tag-schema","heading":"Add the tag schema"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Update ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/schema.ts"]}," to register a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["weather"]}," tag."," ","The ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["render"]}," value must match the exported component name (",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CurrentWeather"]},"), and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["selfClosing"]}," means the tag has no children."," ","Both ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["units"]}," are optional -- when ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["location"]}," is omitted the API function geolocates the visitor by IP address."]}]},{"$$mdtype":"Tag","name":"CodeStep","attributes":{"id":"tag-export","heading":"Export the component"},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Export ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["CurrentWeather"]}," from ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["@theme/markdoc/components.tsx"]}," so the Markdoc runtime can resolve the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["render"]}," value."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"use-the-tag-in-markdown","__idx":6},"children":["Use the tag in Markdown"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Authors can embed the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["weather"]}," tag in any ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":[".md"]}," file."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Geolocate the visitor by IP address:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"markdoc","header":{"controls":{"copy":{}}},"source":"{% weather /%}\n","lang":"markdoc"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Or specify a city explicitly:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"markdoc","header":{"controls":{"copy":{}}},"source":"{% weather location=\"London\" units=\"celsius\" /%}\n","lang":"markdoc"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"resources","__idx":7},"children":["Resources"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/build-markdoc-tags"},"children":["Build Markdoc tags"]}]}," - Create custom Markdoc tags with React components"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":[{"$$mdtype":"Tag","name":"Link","attributes":{"href":"/docs/realm/customization/api-functions/api-functions-reference"},"children":["API functions reference"]}]}," - Function signature, routing, context helpers, and access control"]}]}]}]},"frontmatter":{"products":["Realm","Reef"],"plans":["Pro","Enterprise","Enterprise+"]},"tagList":["code-walkthrough","html","step"],"title":"API functions tutorial: render weather data in a Markdoc tag","lastModified":"2026-10-01T23:00:57.000Z"}