PageSourceSearch

https://backstage.io/assets/js/549deb10.296378d7.js

js backstage.io collected 2026-09-24 08:29:06 UTC 7,087 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkbackstage_microsite=self.webpackChunkbackstage_microsite||[]).push([["16866"],{968377(e,n,t){t.r(n),t.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>h,frontMatter:()=>r,metadata:()=>o,toc:()=>c});var o=t(179389),s=t(474848),i=t(28453);let r={id:"http-client",sidebar_label:"004 - HTTP Client",title:"004 - HTTP Client",description:"How to build an HTTP client for your frontend plugin to fetch backend data"},a,d={},c=[{value:"How the scaffolded code works",id:"how-the-scaffolded-code-works",level:2},{value:"Trying it out",id:"trying-it-out",level:2},{value:"Extracting a client class",id:"extracting-a-client-class",level:2},{value:"OpenAPI generated clients",id:"openapi-generated-clients",level:2}];function l(e){let n={a:"a",admonition:"admonition",code:"code",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",...(0,i.R)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(n.p,{children:["The scaffolded ",(0,s.jsx)(n.code,{children:"TodoPage"})," already fetches data from the backend. Let's look at\nhow that works and how you can extend it."]}),"\n",(0,s.jsx)(n.h2,{id:"how-the-scaffolded-code-works",children:"How the scaffolded code works"}),"\n",(0,s.jsxs)(n.p,{children:["Open ",(0,s.jsx)(n.code,{children:"plugins/todo/src/components/TodoPage/TodoPage.tsx"})," and look at the\n",(0,s.jsx)(n.code,{children:"useTodos"})," hook:"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-tsx",children:"function useTodos() {\n  const { fetch } = useApi(fetchApiRef);\n\n  return useAsync(async (): Promise<TodoItem[]> => {\n    const response = await fetch(`plugin://todo/todos`);\n\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch todos: ${response.status} ${response.statusText}`,\n      );\n    }\n\n    const data = await response.json();\n    return data.items;\n  });\n}\n"})}),"\n",(0,s.jsxs)(n.p,{children:["Here, we're using Backstage's ",(0,s.jsx)(n.code,{children:"fetchApi"})," which wraps the browser ",(0,s.jsx)(n.code,{children:"fetch"})," and automatically does 2 things,"]}),"\n",(0,s.jsxs)(n.ol,{children:["\n",(0,s.jsxs)(n.li,{children:["Injects authentication credentials - you don't need to attach any ",(0,s.jsx)(n.code,{children:"Authorization"})," headers manually."]}),"\n",(0,s.jsxs)(n.li,{children:["Resolves ",(0,s.jsx)(n.code,{children:"plugin://<pluginId>"})," URL schemes to the real plugin URL for your instance."]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["The ",(0,s.jsx)(n.code,{children:"useAsync"})," hook from ",(0,s.jsx)(n.code,{children:"@react-hookz/web"})," runs the async function on mount and\nreturns ",(0,s.jsx)(n.code,{children:"[{ status, result, error }, { execute }]"}),", which the component uses to show a\nloading spinner, example todo items if the backend request fails, or the\nfetched todo list."]}),"\n",(0,s.jsx)(n.h2,{id:"trying-it-out",children:"Trying it out"}),"\n",(0,s.jsxs)(n.p,{children:["Make sure both the frontend and backend are running (",(0,s.jsx)(n.code,{children:"yarn start"})," from the\nrepository root starts both). Navigate to ",(0,s.jsx)(n.code,{children:"http://localhost:3000/todo"})," and\nyou should see todos fetched from your backend."]}),"\n",(0,s.jsx)(n.admonition,{type:"tip",children:(0,s.jsxs)(n.p,{children:["You can create todos using ",(0,s.jsx)(n.code,{children:"curl"})," as described in the\n",(0,s.jsx)(n.a,{href:"/docs/golden-path/plugins/backend/poking-around",children:"backend golden path"}),", then refresh the\nfrontend page to see them appear."]})}),"\n",(0,s.jsx)(n.h2,{id:"extracting-a-client-class",children:"Extracting a client class"}),"\n","\n",(0,s.jsxs)(n.p,{children:["For plugins with several endpoints, extracting a dedicated client class\nkeeps your components focused on rendering. Create\n",(0,s.jsx)(n.code,{children:"plugins/todo/src/api/TodoClient.ts"}),":"]}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-ts",children:"import { FetchApi } from '@backstage/frontend-plugin-api';\nimport type { TodoItem } from '../components/TodoList';\n\nexport class TodoClient {\n  readonly #fetchApi: FetchApi;\n\n  constructor(options: { fetchApi: FetchApi }) {\n    this.#fetchApi = options.fetchApi;\n  }\n\n  async listTodos(): Promise<TodoItem[]> {\n    const response = await this.#fetchApi.fetch(`plugin://todo/todos`);\n\n    if (!response.ok) {\n      throw new Error(\n        `Failed to fetch todos: ${response.status} ${response.statusText}`,\n      );\n    }\n\n    const data = await response.json();\n    return data.items;\n  }\n\n  async createTodo(title: string): Promise<TodoItem> {\n    const response = await this.#fetchApi.fetch(`plugin://todo/todos`, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body: JSON.stringify({ title }),\n    });\n\n    if (!response.ok) {\n      throw new Error(\n        `Failed to create todo: ${response.status} ${response.statusText}`,\n      );\n    }\n\n    return response.json();\n  }\n}\n"})}),"\n",(0,s.jsx)(n.p,{children:"This is optional for the scaffolded example, but becomes valuable as\nyour plugin grows."}),"\n",(0,s.jsx)(n.h2,{id:"openapi-generated-clients",children:"OpenAPI generated clients"}),"\n",(0,s.jsxs)(n.p,{children:["You can also keep your frontend and backend in sync by generating the\nclient from an OpenAPI schema. If your backend plugin exposes an OpenAPI\nspec (see the\n",(0,s.jsx)(n.a,{href:"/docs/golden-path/plugins/backend/first-steps",children:"backend golden path"})," for details),\nyou can generate a type-safe client that updates automatically whenever the\nAPI changes. This approach reduces the risk of the frontend and backend\ndrifting apart over time."]})]})}function h(e={}){let{wrapper:n}={...(0,i.R)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},28453(e,n,t){t.d(n,{R:()=>r,x:()=>a});var o=t(296540);let s={},i=o.createContext(s);function r(e){let n=o.useContext(i);return o.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:r(e.components),o.createElement(i.Provider,{value:n},e.children)}},179389(e){e.exports=JSON.parse('{"id":"golden-path/plugins/frontend/http-cl
1ient","title":"004 - HTTP Client","description":"How to build an HTTP client for your frontend plugin to fetch backend data","source":"@site/versioned_docs/version-stable/golden-path/plugins/frontend/004-http-client.md","sourceDirName":"golden-path/plugins/frontend","slug":"/golden-path/plugins/frontend/http-client","permalink":"/docs/golden-path/plugins/frontend/http-client","draft":false,"unlisted":false,"editUrl":"https://github.com/backstage/backstage/edit/master/docs/golden-path/plugins/frontend/004-http-client.md","tags":[],"version":"stable","sidebarPosition":4,"frontMatter":{"id":"http-client","sidebar_label":"004 - HTTP Client","title":"004 - HTTP Client","description":"How to build an HTTP client for your frontend plugin to fetch backend data"}}')}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.