|
| 1 | +# Coder API - Quick-start guide |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +The Coder plugin makes it easy to bring the entire Coder API into your Backstage deployment. This guide covers how to get it set up so that you can start accessing Coder from Backstage. |
| 6 | + |
| 7 | +Note: this covers the main expected use cases with the plugin. For more information and options on customizing your Backstage deployment further, see our [Advanced API guide](./coder-api-advanced.md). |
| 8 | + |
| 9 | +### Before you begin |
| 10 | + |
| 11 | +Please ensure that you have the Coder plugin fully installed before proceeding. You can find instructions for getting up and running in [our main README](../../README.md). |
| 12 | + |
| 13 | +### Important hooks for using the Coder API |
| 14 | + |
| 15 | +The Coder plugin exposes three (soon to be four) main hooks for accessing Coder plugin state and making queries/mutations |
| 16 | + |
| 17 | +- `useCoderAuth` - Provides methods and state values for interacting with your current Coder auth session from within Backstage. |
| 18 | + |
| 19 | + ```tsx |
| 20 | + function SessionTokenInputForm() { |
| 21 | + const [sessionTokenDraft, setSessionTokenDraft] = useState(''); |
| 22 | + const coderAuth = useCoderAuth(); |
| 23 | + |
| 24 | + const onSubmit = (event: FormEvent<HTMLFormElement>) => { |
| 25 | + coderAuth.registerNewToken(sessionToken); |
| 26 | + setSessionTokenDraft(''); |
| 27 | + }; |
| 28 | + |
| 29 | + return ( |
| 30 | + <form onSubmit={onSubmit}> |
| 31 | + <MainFormContent /> |
| 32 | + </form> |
| 33 | + ); |
| 34 | + } |
| 35 | + ``` |
| 36 | + |
| 37 | +- `useCoderQuery` - Makes it simple to query data from the Coder API and share it throughout your application. |
| 38 | + |
| 39 | + ```tsx |
| 40 | + function WorkspacesList() { |
| 41 | + // Return type matches the return type of React Query's useQuerys |
| 42 | + const workspacesQuery = useCoderQuery({ |
| 43 | + queryKey: ['workspaces'], |
| 44 | + queryFn: ({ coderApi }) => coderApi.getWorkspaces({ limit: 5 }), |
| 45 | + }); |
| 46 | + } |
| 47 | + ``` |
| 48 | + |
| 49 | +- `useCoderMutation` (coming soon) - Makes it simple to mutate data via the Coder API. |
| 50 | +- `useCoderApi` - Exposes an object with all available Coder API methods. None of the state in this object is tied to React render logic - it can be treated as a "function bucket". Once `useCoderMutation` is available, the main value of this hook will be as an escape hatch in the rare situations where `useCoderQuery` and `useCoderMutation` don't meet your needs. Under the hood, both `useCoderQuery` and `useCoderMutation` receive their `coderApi` context value from this hook. |
| 51 | + |
| 52 | + ```tsx |
| 53 | + function HealthCheckComponent() { |
| 54 | + const coderApi = useCoderApi(); |
| 55 | + |
| 56 | + const processWorkspaces = async () => { |
| 57 | + const workspacesResponse = await coderApi.getWorkspaces({ |
| 58 | + limit: 10, |
| 59 | + }); |
| 60 | + |
| 61 | + processHealthChecks(workspacesResponse.workspaces); |
| 62 | + }; |
| 63 | + } |
| 64 | + ``` |
| 65 | + |
| 66 | +Internally, the Coder plugin uses [React Query/TanStack Query v4](https://tanstack.com/query/v4/docs/framework/react/overview). In fact, `useCoderQuery` and `useCoderMutation` are simply wrappers over `useQuery` and `useMutation`. Both simplify the process of wiring up the hooks' various properties to the Coder auth, while exposing a more convenient way of accessing the Coder API object. |
| 67 | + |
| 68 | +If you ever need to coordinate queries and mutations, you can use `useQueryClient` from React Query - no custom plugin-specific hook needed. |
| 69 | + |
| 70 | +The bottom of this document has examples of both queries and mutations. |
| 71 | + |
| 72 | +### Grouping queries with the Coder query key prefix |
| 73 | + |
| 74 | +The plugin exposes a `CODER_QUERY_KEY_PREFIX` constant that you can use to group all Coder queries. `useCoderQuery` automatically injects this value into all its `queryKey` arrays. However, if you need to escape out with `useQuery`, you can import the constant and manually include it as the first value of your query key. |
| 75 | + |
| 76 | +In addition, all official Coder plugin components use this prefix internally. |
| 77 | + |
| 78 | +```tsx |
| 79 | +// All grouped queries can be invalidated at once from the query client |
| 80 | +const queryClient = useQueryClient(); |
| 81 | +const invalidateAllCoderQueries = () => { |
| 82 | + queryClient.invalidateQuery({ |
| 83 | + queryKey: [CODER_QUERY_KEY_PREFIX], |
| 84 | + }); |
| 85 | +}; |
| 86 | + |
| 87 | +// The prefix is only needed when NOT using useCoderQuery |
| 88 | +const customQuery = useQuery({ |
| 89 | + queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'], |
| 90 | + queryFn: () => { |
| 91 | + // Your custom API logic |
| 92 | + }, |
| 93 | +}); |
| 94 | + |
| 95 | +// When the user unlinks their session token, all queries grouped under |
| 96 | +// CODER_QUERY_KEY_PREFIX are vacated from the active query cache |
| 97 | +function LogOutButton() { |
| 98 | + const { unlinkToken } = useCoderAuth(); |
| 99 | + |
| 100 | + return ( |
| 101 | + <button type="button" onClick={unlinkToken}> |
| 102 | + Unlink Coder account |
| 103 | + </button> |
| 104 | + ); |
| 105 | +} |
| 106 | +``` |
| 107 | + |
| 108 | +## Recommendations for accessing the API |
| 109 | + |
| 110 | +1. If querying data, prefer `useCoderQuery`. It automatically wires up all auth logic to React Query (which includes pausing queries if the user is not authenticated). It also lets you access the Coder API via its query function. `useQuery` works as an escape hatch if `useCoderQuery` doesn't meet your needs, but it requires more work to wire up correctly. |
| 111 | +2. If mutating data, you will need to call `useMutation`, `useQueryClient`, and `useCoderApi` in tandem\*. |
| 112 | + |
| 113 | +We highly recommend **not** fetching with `useState` + `useEffect`, or with `useAsync`. Both face performance issues when trying to share state. See [ui.dev](https://www.ui.dev/)'s wonderful [_The Story of React Query_ video](https://www.youtube.com/watch?v=OrliU0e09io) for more info on some of the problems they face. |
| 114 | + |
| 115 | +\* `useCoderMutation` can be used instead of all three once that hook is available. |
| 116 | + |
| 117 | +### Comparing query caching strategies |
| 118 | + |
| 119 | +| | `useAsync` | `useQuery` | `useCoderQuery` | |
| 120 | +| ------------------------------------------------------------------ | ---------- | ---------- | --------------- | |
| 121 | +| Automatically handles race conditions | ✅ | ✅ | ✅ | |
| 122 | +| Can retain state after component unmounts | 🚫 | ✅ | ✅ | |
| 123 | +| Easy, on-command query invalidation | 🚫 | ✅ | ✅ | |
| 124 | +| Automatic retry logic when a query fails | 🚫 | ✅ | ✅ | |
| 125 | +| Less need to fight dependency arrays | 🚫 | ✅ | ✅ | |
| 126 | +| Easy to share state for sibling components | 🚫 | ✅ | ✅ | |
| 127 | +| Pre-wired to Coder auth logic | 🚫 | 🚫 | ✅ | |
| 128 | +| Can consume Coder API directly from query function | 🚫 | 🚫 | ✅ | |
| 129 | +| Automatically groups Coder-related queries by prefixing query keys | 🚫 | 🚫 | ✅ | |
| 130 | + |
| 131 | +## Authentication |
| 132 | + |
| 133 | +All API calls to **any** of the Coder API functions will fail if you have not authenticated yet. Authentication can be handled via any of the official Coder components that can be imported via the plugin. However, if there are no Coder components on the screen, the `CoderProvider` component will automatically\* inject a fallback auth button for letting the user add their auth info. |
| 134 | + |
| 135 | +https://github.com/coder/backstage-plugins/assets/28937484/0ece4410-36fc-4b32-9223-66f35953eeab |
| 136 | + |
| 137 | +Once the user has been authenticated, all Coder API functions will become available. When the user unlinks their auth token (effectively logging out), all cached queries that start with `CODER_QUERY_KEY_PREFIX` will automatically be vacated. |
| 138 | + |
| 139 | +\* This behavior can be disabled. Please see our [advanced API guide](./coder-api-advanced.md) for more information. |
| 140 | + |
| 141 | +## Component examples |
| 142 | + |
| 143 | +Here are some full code examples showcasing patterns you can bring into your own codebase. |
| 144 | + |
| 145 | +Note: To keep the examples simple, none of them contain any CSS styling or MUI components. |
| 146 | + |
| 147 | +### Displaying recent audit logs |
| 148 | + |
| 149 | +```tsx |
| 150 | +import React from 'react'; |
| 151 | +import { useCoderQuery } from '@coder/backstage-plugin-coder'; |
| 152 | + |
| 153 | +function RecentAuditLogsList() { |
| 154 | + const auditLogsQuery = useCoderQuery({ |
| 155 | + queryKey: ['audits', 'logs'], |
| 156 | + queryFn: ({ coderApi }) => coderApi.getAuditLogs({ limit: 10 }), |
| 157 | + }); |
| 158 | + |
| 159 | + return ( |
| 160 | + <> |
| 161 | + {auditLogsQuery.isLoading && <p>Loading…</p>} |
| 162 | + {auditLogsQuery.error instanceof Error && ( |
| 163 | + <p>Encountered the following error: {auditLogsQuery.error.message}</p> |
| 164 | + )} |
| 165 | + |
| 166 | + {auditLogsQuery.data !== undefined && ( |
| 167 | + <ul> |
| 168 | + {auditLogsQuery.data.audit_logs.map(log => ( |
| 169 | + <li key={log.id}>{log.description}</li> |
| 170 | + ))} |
| 171 | + </ul> |
| 172 | + )} |
| 173 | + </> |
| 174 | + ); |
| 175 | +} |
| 176 | +``` |
| 177 | + |
| 178 | +## Creating a new workspace |
| 179 | + |
| 180 | +Note: this example showcases how to perform mutations with `useMutation`. The example will be updated once `useCoderMutation` is available. |
| 181 | + |
| 182 | +```tsx |
| 183 | +import React, { type FormEvent, useState } from 'react'; |
| 184 | +import { useMutation, useQueryClient } from '@tanstack/react-query'; |
| 185 | +import { |
| 186 | + type CreateWorkspaceRequest, |
| 187 | + CODER_QUERY_KEY_PREFIX, |
| 188 | + useCoderQuery, |
| 189 | + useCoderApi, |
| 190 | +} from '@coder/backstage-plugin-coder'; |
| 191 | + |
| 192 | +export function WorkspaceCreationForm() { |
| 193 | + const [newWorkspaceName, setNewWorkspaceName] = useState(''); |
| 194 | + const coderApi = useCoderSdk(); |
| 195 | + const queryClient = useQueryClient(); |
| 196 | + |
| 197 | + const currentUserQuery = useCoderQuery({ |
| 198 | + queryKey: ['currentUser'], |
| 199 | + queryFn: coderApi.getAuthenticatedUser, |
| 200 | + }); |
| 201 | + |
| 202 | + const workspacesQuery = useCoderQuery({ |
| 203 | + queryKey: ['workspaces'], |
| 204 | + queryFn: coderApi.getWorkspaces, |
| 205 | + }); |
| 206 | + |
| 207 | + const createWorkspaceMutation = useMutation({ |
| 208 | + mutationFn: (payload: CreateWorkspaceRequest) => { |
| 209 | + if (currentUserQuery.data === undefined) { |
| 210 | + throw new Error( |
| 211 | + 'Cannot create workspace without data for current user', |
| 212 | + ); |
| 213 | + } |
| 214 | + |
| 215 | + const { organization_ids, id: userId } = currentUserQuery.data; |
| 216 | + return coderApi.createWorkspace(organization_ids[0], userId, payload); |
| 217 | + }, |
| 218 | + }); |
| 219 | + |
| 220 | + const onSubmit = async (event: FormEvent<HTMLFormElement>) => { |
| 221 | + event.preventDefault(); |
| 222 | + |
| 223 | + // If the mutation fails, useMutation will expose the error in the UI via |
| 224 | + // its own exposed properties |
| 225 | + await createWorkspaceMutation.mutateAsync({ |
| 226 | + name: newWorkspaceName, |
| 227 | + }); |
| 228 | + |
| 229 | + setNewWorkspaceName(''); |
| 230 | + queryClient.invalidateQueries({ |
| 231 | + queryKey: [CODER_QUERY_KEY_PREFIX, 'workspaces'], |
| 232 | + }); |
| 233 | + }; |
| 234 | + |
| 235 | + return ( |
| 236 | + <> |
| 237 | + {createWorkspaceMutation.isSuccess && ( |
| 238 | + <p> |
| 239 | + Workspace {createWorkspaceMutation.data.name} created successfully! |
| 240 | + </p> |
| 241 | + )} |
| 242 | + |
| 243 | + <form onSubmit={onSubmit}> |
| 244 | + <fieldset> |
| 245 | + <legend>Required fields</legend> |
| 246 | + |
| 247 | + <label> |
| 248 | + Workspace name |
| 249 | + <input |
| 250 | + type="text" |
| 251 | + value={newWorkspaceName} |
| 252 | + onChange={event => setNewWorkspaceName(event.target.value)} |
| 253 | + /> |
| 254 | + </label> |
| 255 | + </fieldset> |
| 256 | + |
| 257 | + <button type="submit">Create workspace</button> |
| 258 | + </form> |
| 259 | + </> |
| 260 | + ); |
| 261 | +} |
| 262 | +``` |
0 commit comments