React에서 FastAPI로
connection 생성기는 React 웹사이트를 FastAPI 백엔드와 빠르게 통합할 수 있는 방법을 제공합니다. 클라이언트 및 TanStack Query 훅 생성, AWS IAM 및 Cognito 인증 지원, 적절한 오류 처리를 포함하여 FastAPI 백엔드에 연결하는 데 필요한 모든 구성을 타입 안전한 방식으로 설정합니다.
전제 조건
섹션 제목: “전제 조건”이 생성기를 사용하기 전에 React 애플리케이션에 다음이 있어야 합니다:
- 애플리케이션을 렌더링하는
main.tsx파일 - 작동하는 FastAPI 백엔드 (FastAPI 생성기를 사용하여 생성)
- Cognito 또는 IAM 인증을 사용하는 API에 연결하는 경우
ts#website#auth생성기를 통해 추가된 Cognito Auth
필수 main.tsx 구조 예시
import { StrictMode } from 'react';import * as ReactDOM from 'react-dom/client';import App from './app/app';
const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement,);root.render( <StrictMode> <App /> </StrictMode>,);사용법
섹션 제목: “사용법”생성기 실행
섹션 제목: “생성기 실행”pnpm nx g @aws/nx-plugin:connectionyarn nx g @aws/nx-plugin:connectionnpx nx g @aws/nx-plugin:connectionbunx nx g @aws/nx-plugin:connection- 설치 Nx Console VSCode Plugin 아직 설치하지 않았다면
- VSCode에서 Nx 콘솔 열기
- 클릭
Generate (UI)"Common Nx Commands" 섹션에서 - 검색
@aws/nx-plugin - connection - 필수 매개변수 입력
- 클릭
Generate
| 매개변수 | 타입 | 기본값 | 설명 |
|---|---|---|---|
| sourceProject 필수 | string | - | 소스 프로젝트 |
| targetProject 필수 | string | - | 연결할 대상 프로젝트 |
| sourceComponent | string | - | 연결을 시작할 소스 컴포넌트 (컴포넌트 이름, 소스 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 소스로 명시적으로 선택하려면 '.'을 사용하세요. |
| targetComponent | string | - | 연결할 대상 컴포넌트 (컴포넌트 이름, 대상 프로젝트 루트 기준 상대 경로, 또는 generator id). 프로젝트를 대상으로 명시적으로 선택하려면 '.'을 사용하세요. |
| preferInstallDependencies | boolean | true | 생성기 실행 후 의존성 설치를 선호할지 여부입니다. 여러 생성기를 일괄 처리할 때 설치를 연기하려면 false로 설정하세요 (후속 생성기가 Nx 프로젝트 그래프를 계산할 수 있도록 필요한 경우 설치는 여전히 실행됩니다); 마지막에 한 번만 설치합니다. |
생성기 출력
섹션 제목: “생성기 출력”생성기는 FastAPI 프로젝트의 다음 파일을 변경합니다:
디렉터리scripts
- generate_open_api.py API에 대한 OpenAPI 사양을 생성하는 스크립트 추가
- project.json 위의 생성 스크립트를 호출하는 새 타겟이 빌드에 추가됨
생성기는 React 애플리케이션의 다음 파일을 변경합니다:
디렉터리src
디렉터리components
- <ApiName>Provider.tsx API 클라이언트를 위한 Provider
- QueryClientProvider.tsx TanStack React Query 클라이언트 provider
디렉터리hooks
- use<ApiName>.tsx TanStack Query로 상태가 관리되는 API 호출을 위한 훅 추가
- use<ApiName>Client.tsx API를 호출할 수 있는 바닐라 API 클라이언트를 인스턴스화하는 훅 추가
- useSigV4.tsx SigV4로 HTTP 요청에 서명하는 훅 추가 (IAM 인증을 선택한 경우)
- project.json 타입 안전 클라이언트를 생성하는 새 타겟이 빌드에 추가됨
- .gitignore 생성된 클라이언트 파일은 기본적으로 무시됨
생성기는 또한 아직 없는 경우 웹사이트 인프라에 Runtime Config를 추가하여 FastAPI의 API URL이 웹사이트에서 사용 가능하고 use<ApiName>.tsx 훅에 의해 자동으로 구성되도록 합니다.
코드 생성
섹션 제목: “코드 생성”빌드 시 FastAPI의 OpenAPI 사양에서 타입 안전 클라이언트가 생성됩니다. 이는 React 애플리케이션에 세 개의 새 파일을 추가합니다:
디렉터리src
디렉터리generated
디렉터리<ApiName>
- types.gen.ts FastAPI에 정의된 pydantic 모델에서 생성된 타입
- client.gen.ts API 호출을 위한 타입 안전 클라이언트
- options-proxy.gen.ts TanStack Query를 사용하여 API와 상호 작용하기 위한 TanStack Query 훅 옵션을 생성하는 메서드 제공
생성된 코드 사용
섹션 제목: “생성된 코드 사용”생성된 타입 안전 클라이언트를 사용하여 React 애플리케이션에서 FastAPI를 호출할 수 있습니다. TanStack Query 훅을 통해 클라이언트를 사용하는 것이 권장되지만, 원하는 경우 바닐라 클라이언트를 사용할 수도 있습니다.
파일 감시자 의존성
watch-generate:<ApiName>-client는 nx watch 명령에 의존하며, 이는 Nx Daemon이 실행 중이어야 합니다. 따라서 데몬을 비활성화한 경우 FastAPI를 변경할 때 클라이언트가 자동으로 재생성되지 않습니다.
API 훅 사용
섹션 제목: “API 훅 사용”생성기는 TanStack Query로 API를 호출하는 데 사용할 수 있는 use<ApiName> 훅을 제공합니다.
queryOptions 메서드를 사용하여 TanStack Query의 useQuery 훅을 사용하여 API를 호출하는 데 필요한 옵션을 검색할 수 있습니다:
import { useQuery } from '@tanstack/react-query';import { useState, useEffect } from 'react';import { useMyApi } from './hooks/useMyApi';
function MyComponent() { const api = useMyApi(); const item = useQuery(api.getItem.queryOptions({ itemId: 'some-id' }));
if (item.isLoading) return <div>Loading...</div>; if (item.isError) return <div>Error: {item.error.message}</div>;
return <div>Item: {item.data.name}</div>;}API 클라이언트를 직접 사용
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function MyComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItem = async () => { try { const data = await api.getItem({ itemId: 'some-id' }); setItem(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItem(); }, [api]);
if (loading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>Item: {item.name}</div>;}뮤테이션
섹션 제목: “뮤테이션”생성된 훅에는 TanStack Query의 useMutation 훅을 사용한 뮤테이션 지원이 포함되어 있습니다. 이는 로딩 상태, 오류 처리 및 낙관적 업데이트를 통해 생성, 업데이트 및 삭제 작업을 처리하는 깔끔한 방법을 제공합니다.
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function CreateItemForm() { const api = useMyApi(); // Create a mutation using the generated mutation options const createItem = useMutation(api.createItem.mutationOptions());
const handleSubmit = (e) => { e.preventDefault(); createItem.mutate({ name: 'New Item', description: 'A new item' }); };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button>
{createItem.isSuccess && ( <div className="success"> Item created with ID: {createItem.data.id} </div> )}
{createItem.isError && ( <div className="error"> Error: {createItem.error.message} </div> )} </form> );}다양한 뮤테이션 상태에 대한 콜백 추가도 가능합니다:
const createItem = useMutation({ ...api.createItem.mutationOptions(), onSuccess: (data) => { // This will run when the mutation succeeds console.log('Item created:', data); // You can navigate to the new item navigate(`/items/${data.id}`); }, onError: (error) => { // This will run when the mutation fails console.error('Failed to create item:', error); }, onSettled: () => { // This will run when the mutation completes (success or error) // Good place to invalidate queries that might be affected queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }});API 클라이언트를 직접 사용한 뮤테이션
import { useState } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function CreateItemForm() { const api = useMyApiClient(); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [createdItem, setCreatedItem] = useState(null);
const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(null);
try { const newItem = await api.createItem({ name: 'New Item', description: 'A new item' }); setCreatedItem(newItem); // You can navigate to the new item // navigate(`/items/${newItem.id}`); } catch (err) { setError(err); console.error('Failed to create item:', err); } finally { setIsLoading(false); } };
return ( <form onSubmit={handleSubmit}> {/* Form fields */} <button type="submit" disabled={isLoading} > {isLoading ? 'Creating...' : 'Create Item'} </button>
{createdItem && ( <div className="success"> Item created with ID: {createdItem.id} </div> )}
{error && ( <div className="error"> Error: {error.message} </div> )} </form> );}파일 업로드
섹션 제목: “파일 업로드”파일 업로드를 허용하는 엔드포인트의 경우 생성된 클라이언트는 요청을 FormData로 전송합니다. FastAPI에서 multipart/form-data 본문을 사용하는 작업을 정의하세요. 예를 들어 UploadFile을 사용:
from fastapi import UploadFile
@app.post("/files")async def upload_file(file: UploadFile, description: str = "") -> FileMetadata: contents = await file.read() ...바이너리 필드는 생성된 클라이언트에서 Blob 타입으로 지정되며, 다른 필드(위의 description 등)는 모델링된 타입을 유지합니다. Blob 또는 File을 전달하세요 — 예를 들어 <input type="file">에서 얻은 것:
import { useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function UploadForm() { const api = useMyApi(); const uploadFile = useMutation(api.uploadFile.mutationOptions());
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) { uploadFile.mutate({ file, description: file.name }); } };
return <input type="file" onChange={handleChange} />;}클라이언트는 FormData 본문을 빌드하고 fetch가 Content-Type(multipart boundary 포함)을 자동으로 설정하도록 합니다.
무한 쿼리를 사용한 페이지네이션
섹션 제목: “무한 쿼리를 사용한 페이지네이션”입력으로 cursor 매개변수를 허용하는 엔드포인트의 경우 생성된 훅은 TanStack Query의 useInfiniteQuery 훅을 사용한 무한 쿼리 지원을 제공합니다. 이를 통해 “더 보기” 또는 무한 스크롤 기능을 쉽게 구현할 수 있습니다.
import { useInfiniteQuery } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemList() { const api = useMyApi(); const items = useInfiniteQuery({ ...api.listItems.infiniteQueryOptions({ limit: 10, // Number of items per page }, { // Make sure you define a getNextPageParam function to return // the parameter that should be passed as the 'cursor' for the // next page getNextPageParam: (lastPage) => lastPage.nextCursor || undefined }), });
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { return <ErrorMessage message={items.error.message} />; }
return ( <div> {/* Flatten the pages array to render all items */} <ul> {items.data.pages.flatMap(page => page.items.map(item => ( <li key={item.id}>{item.name}</li> )) )} </ul>
<button onClick={() => items.fetchNextPage()} disabled={!items.hasNextPage || items.isFetchingNextPage} > {items.isFetchingNextPage ? 'Loading more...' : items.hasNextPage ? 'Load More' : 'No more items'} </button> </div> );}생성된 훅은 API가 지원하는 경우 커서 기반 페이지네이션을 자동으로 처리합니다. nextCursor 값은 응답에서 추출되어 다음 페이지를 가져오는 데 사용됩니다.
API 클라이언트를 직접 사용한 페이지네이션
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [nextCursor, setNextCursor] = useState(null); const [isFetchingMore, setIsFetchingMore] = useState(false);
// Fetch initial data useEffect(() => { const fetchItems = async () => { try { setIsLoading(true); const response = await api.listItems({ limit: 10 }); setItems(response.items); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsLoading(false); } };
fetchItems(); }, [api]);
// Function to load more items const loadMore = async () => { if (!nextCursor) return;
try { setIsFetchingMore(true); const response = await api.listItems({ limit: 10, cursor: nextCursor });
setItems(prevItems => [...prevItems, ...response.items]); setNextCursor(response.nextCursor); } catch (err) { setError(err); } finally { setIsFetchingMore(false); } };
if (isLoading) { return <LoadingSpinner />; }
if (error) { return <ErrorMessage message={error.message} />; }
return ( <div> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul>
<button onClick={loadMore} disabled={!nextCursor || isFetchingMore} > {isFetchingMore ? 'Loading more...' : nextCursor ? 'Load More' : 'No more items'} </button> </div> );}오류 처리
섹션 제목: “오류 처리”통합에는 타입이 지정된 오류 응답과 함께 내장된 오류 처리가 포함되어 있습니다. OpenAPI 사양에 정의된 가능한 오류 응답을 캡슐화하는 <operation-name>Error 타입이 생성됩니다. 각 오류에는 status 및 error 속성이 있으며, status 값을 확인하여 특정 오류 타입으로 좁힐 수 있습니다.
import { useMutation } from '@tanstack/react-query';
function MyComponent() { const api = useMyApi(); const createItem = useMutation(api.createItem.mutationOptions());
const handleClick = () => { createItem.mutate({ name: 'New Item' }); };
if (createItem.error) { switch (createItem.error.status) { case 400: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{createItem.error.error.message}</p> <ul> {createItem.error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{createItem.error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{createItem.error.error.message}</p> <p>Trace ID: {createItem.error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}API 클라이언트를 직접 사용한 오류 처리
function MyComponent() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleClick = async () => { try { await api.createItem({ name: 'New Item' }); } catch (e) { const err = e as CreateItemError; setError(err); } };
if (error) { switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <div> <h2>Invalid input:</h2> <p>{error.error.message}</p> <ul> {error.error.validationErrors.map((err) => ( <li key={err.field}>{err.message}</li> ))} </ul> </div> ); case 403: // error.error is typed as CreateItem403Response return ( <div> <h2>Not authorized:</h2> <p>{error.error.reason}</p> </div> ); case 500: case 502: // error.error is typed as CreateItem5XXResponse return ( <div> <h2>Server error:</h2> <p>{error.error.message}</p> <p>Trace ID: {error.error.traceId}</p> </div> ); } }
return <button onClick={handleClick}>Create Item</button>;}스트림 소비
섹션 제목: “스트림 소비”FastAPI가 응답을 스트리밍하도록 구성한 경우, useQuery 훅은 스트림의 새 청크가 도착할 때마다 자동으로 데이터를 업데이트합니다.
예시:
function MyStreamingComponent() { const api = useMyApi(); const stream = useQuery(api.myStream.queryOptions());
return ( <ul> {(stream.data ?? []).map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}필요한 경우 isLoading 및 fetchStatus 속성을 사용하여 스트림의 현재 상태를 확인할 수 있습니다. 스트림은 다음 수명 주기를 따릅니다:
-
스트리밍을 시작하기 위한 HTTP 요청이 전송됨
isLoading은truefetchStatus는'fetching'data는undefined
-
스트림의 첫 번째 청크가 수신됨
isLoading은false가 됨fetchStatus는'fetching'으로 유지data는 첫 번째 청크를 포함하는 배열이 됨
-
후속 청크가 수신됨
isLoading은false로 유지fetchStatus는'fetching'으로 유지data는 수신되는 즉시 각 후속 청크로 업데이트됨
-
스트림이 완료됨
isLoading은false로 유지fetchStatus는'idle'이 됨data는 수신된 모든 청크의 배열
API 클라이언트를 직접 사용한 스트리밍
FastAPI가 응답을 스트리밍하도록 구성한 경우, 생성된 클라이언트에는 for await 구문을 사용하여 스트림의 청크를 비동기적으로 반복하는 타입 안전 메서드가 포함됩니다.
예시:
function MyStreamingComponent() { const api = useMyApiClient();
const [chunks, setChunks] = useState<Chunk[]>([]);
useEffect(() => { const streamChunks = async () => { for await (const chunk of api.myStream()) { setChunks((prev) => [...prev, chunk]); } }; streamChunks(); }, [api]);
return ( <ul> {chunks.map((chunk) => ( <li> {chunk.timestamp.toISOString()}: {chunk.message} </li> ))} </ul> );}생성된 코드 사용자 정의
섹션 제목: “생성된 코드 사용자 정의”쿼리 및 뮤테이션
섹션 제목: “쿼리 및 뮤테이션”기본적으로 HTTP 메서드 PUT, POST, PATCH 및 DELETE를 사용하는 FastAPI의 작업은 뮤테이션으로 간주되고, 다른 모든 작업은 쿼리로 간주됩니다.
x-query와 x-mutation을 사용하여 이 동작을 변경할 수 있습니다.
x-query
섹션 제목: “x-query”@app.post( "/items", openapi_extra={ "x-query": True })def list_items(): # ...생성된 훅은 POST HTTP 메서드를 사용하더라도 queryOptions를 제공합니다:
const items = useQuery(api.listItems.queryOptions());x-mutation
섹션 제목: “x-mutation”@app.get( "/start-processing", openapi_extra={ "x-mutation": True })def start_processing(): # ...생성된 훅은 GET HTTP 메서드를 사용하더라도 mutationOptions를 제공합니다:
// Generated hook will include the custom optionsconst startProcessing = useMutation(api.startProcessing.mutationOptions());사용자 정의 페이지네이션 커서
섹션 제목: “사용자 정의 페이지네이션 커서”기본적으로 생성된 훅은 cursor라는 매개변수를 사용한 커서 기반 페이지네이션을 가정합니다. x-cursor 확장을 사용하여 이 동작을 사용자 정의할 수 있습니다:
@app.get( "/items", openapi_extra={ # Specify a different parameter name for the cursor "x-cursor": "page_token" })def list_items(page_token: str = None, limit: int = 10): # ... return { "items": items, "page_token": next_page_token }작업에 대해 infiniteQueryOptions를 생성하지 않으려면 x-cursor를 False로 설정할 수 있습니다:
@app.get( "/items", openapi_extra={ # Disable cursor-based pagination for this endpoint "x-cursor": False })def list_items(page: int = 1, limit: int = 10): # ... return { "items": items, "total": total_count, "page": page, "pages": total_pages }작업 그룹화
섹션 제목: “작업 그룹화”생성된 훅과 클라이언트 메서드는 FastAPI 엔드포인트의 OpenAPI 태그를 기반으로 자동으로 구성됩니다. 이를 통해 API 호출을 체계적으로 유지하고 관련 작업을 쉽게 찾을 수 있습니다.
예시:
@app.get( "/items", tags=["items"],)def list(): # ...
@app.post( "/items", tags=["items"],)def create(item: Item): # ...@app.get( "/users", tags=["users"],)def list(): # ...생성된 훅은 태그별로 그룹화됩니다:
import { useQuery, useMutation } from '@tanstack/react-query';import { useMyApi } from './hooks/useMyApi';
function ItemsAndUsers() { const api = useMyApi();
// Items operations are grouped under api.items const items = useQuery(api.items.list.queryOptions()); const createItem = useMutation(api.items.create.mutationOptions());
// Users operations are grouped under api.users const users = useQuery(api.users.list.queryOptions());
// Usage example const handleCreateItem = () => { createItem.mutate({ name: 'New Item' }); };
return ( <div> <h2>Items</h2> <ul> {items.data?.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.data?.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}이 그룹화를 통해 API 호출을 더 쉽게 구성하고 IDE에서 더 나은 코드 완성을 제공합니다.
API 클라이언트를 직접 사용한 그룹화된 작업
import { useState, useEffect } from 'react';import { useMyApiClient } from './hooks/useMyApiClient';
function ItemsAndUsers() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true);
// Load data useEffect(() => { const fetchData = async () => { try { setIsLoading(true);
// Items operations are grouped under api.items const itemsData = await api.items.list(); setItems(itemsData);
// Users operations are grouped under api.users const usersData = await api.users.list(); setUsers(usersData); } catch (error) { console.error('Error fetching data:', error); } finally { setIsLoading(false); } };
fetchData(); }, [api]);
const handleCreateItem = async () => { try { // Create item using the grouped method const newItem = await api.items.create({ name: 'New Item' }); setItems(prevItems => [...prevItems, newItem]); } catch (error) { console.error('Error creating item:', error); } };
if (isLoading) { return <div>Loading...</div>; }
return ( <div> <h2>Items</h2> <ul> {items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> <button onClick={handleCreateItem}>Add Item</button>
<h2>Users</h2> <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> );}사용자 정의 예외 클래스, 예외 핸들러를 정의하고 다양한 오류 상태 코드에 대한 응답 모델을 지정하여 FastAPI에서 오류 응답을 사용자 정의할 수 있습니다. 생성된 클라이언트는 이러한 사용자 정의 오류 타입을 자동으로 처리합니다.
사용자 정의 오류 모델 정의
섹션 제목: “사용자 정의 오류 모델 정의”먼저 Pydantic을 사용하여 오류 모델을 정의합니다:
from pydantic import BaseModel
class ErrorDetails(BaseModel): message: str
class ValidationError(BaseModel): message: str field_errors: list[str]사용자 정의 예외 생성
섹션 제목: “사용자 정의 예외 생성”그런 다음 다양한 오류 시나리오에 대한 사용자 정의 예외 클래스를 생성합니다:
class NotFoundException(Exception): def __init__(self, message: str): self.message = message
class ValidationException(Exception): def __init__(self, details: ValidationError): self.details = details예외 핸들러 추가
섹션 제목: “예외 핸들러 추가”예외를 HTTP 응답으로 변환하기 위해 예외 핸들러를 등록합니다:
from fastapi import Requestfrom fastapi.responses import JSONResponse
@app.exception_handler(NotFoundException)async def not_found_handler(request: Request, exc: NotFoundException): return JSONResponse( status_code=404, content=exc.message, )
@app.exception_handler(ValidationException)async def validation_error_handler(request: Request, exc: ValidationException): return JSONResponse( status_code=400, content=exc.details.model_dump(), )응답 모델 지정
섹션 제목: “응답 모델 지정”마지막으로 엔드포인트 정의에서 다양한 오류 상태 코드에 대한 응답 모델을 지정합니다:
@app.get( "/items/{item_id}", responses={ 404: {"model": str} 500: {"model": ErrorDetails} })def get_item(item_id: str) -> Item: item = find_item(item_id) if not item: raise NotFoundException(message=f"Item with ID {item_id} not found") return item
@app.post( "/items", responses={ 400: {"model": ValidationError}, 403: {"model": str} })def create_item(item: Item) -> Item: if not is_valid(item): raise ValidationException( ValidationError( message="Invalid item data", field_errors=["name is required"] ) ) return save_item(item)React에서 사용자 정의 오류 타입 사용
섹션 제목: “React에서 사용자 정의 오류 타입 사용”생성된 클라이언트는 이러한 사용자 정의 오류 타입을 자동으로 처리하여 다양한 오류 응답을 타입 체크하고 처리할 수 있습니다:
import { useMutation, useQuery } from '@tanstack/react-query';
function ItemComponent() { const api = useMyApi();
// Query with typed error handling const getItem = useQuery({ ...api.getItem.queryOptions({ itemId: '123' }), onError: (error) => { // Error is typed based on the responses in your FastAPI switch (error.status) { case 404: // error.error is a string as specified in the responses console.error('Not found:', error.error); break; case 500: // error.error is typed as ErrorDetails console.error('Server error:', error.error.message); break; } } });
// Mutation with typed error handling const createItem = useMutation({ ...api.createItem.mutationOptions(), onError: (error) => { switch (error.status) { case 400: // error.error is typed as ValidationError console.error('Validation error:', error.error.message); console.error('Field errors:', error.error.field_errors); break; case 403: // error.error is a string as specified in the responses console.error('Forbidden:', error.error); break; } } });
// Component rendering with error handling if (getItem.isError) { if (getItem.error.status === 404) { return <NotFoundMessage message={getItem.error.error} />; } else { return <ErrorMessage message={getItem.error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}클라이언트를 직접 사용한 사용자 정의 오류 처리
import { useState, useEffect } from 'react';
function ItemComponent() { const api = useMyApiClient(); const [item, setItem] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(true);
// Fetch item with error handling useEffect(() => { const fetchItem = async () => { try { setLoading(true); const data = await api.getItem({ itemId: '123' }); setItem(data); } catch (e) { // Error is typed based on the responses in your FastAPI const err = e as GetItemError; setError(err);
switch (err.status) { case 404: // err.error is a string as specified in the responses console.error('Not found:', err.error); break; case 500: // err.error is typed as ErrorDetails console.error('Server error:', err.error.message); break; } } finally { setLoading(false); } };
fetchItem(); }, [api]);
// Create item with error handling const handleCreateItem = async (data) => { try { await api.createItem(data); } catch (e) { const err = e as CreateItemError;
switch (err.status) { case 400: // err.error is typed as ValidationError console.error('Validation error:', err.error.message); console.error('Field errors:', err.error.field_errors); break; case 403: // err.error is a string as specified in the responses console.error('Forbidden:', err.error); break; } } };
// Component rendering with error handling if (loading) { return <LoadingSpinner />; }
if (error) { if (error.status === 404) { return <NotFoundMessage message={error.error} />; } else if (error.status === 500) { return <ErrorMessage message={error.error.message} />; } }
return ( <div> {/* Component content */} </div> );}모범 사례
섹션 제목: “모범 사례”로딩 상태 처리
섹션 제목: “로딩 상태 처리”더 나은 사용자 경험을 위해 항상 로딩 및 오류 상태를 처리하세요:
import { useQuery } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const items = useQuery(api.listItems.queryOptions());
if (items.isLoading) { return <LoadingSpinner />; }
if (items.isError) { const err = items.error; switch (err.status) { case 403: // err.error is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.data.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}API 클라이언트를 직접 사용한 로딩 상태 처리
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { const fetchItems = async () => { try { const data = await api.listItems(); setItems(data); } catch (err) { setError(err); } finally { setLoading(false); } }; fetchItems(); }, [api]);
if (loading) { return <LoadingSpinner />; }
if (error) { const err = error as ListItemsError; switch (err.status) { case 403: // err.error is typed as ListItems403Response return <ErrorMessage message={err.error.reason} />; case 500: case 502: // err.error is typed as ListItems5XXResponse return ( <ErrorMessage message={err.error.message} details={`Trace ID: ${err.error.traceId}`} /> ); default: return <ErrorMessage message="An unknown error occurred" />; } }
return ( <ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul> );}낙관적 업데이트
섹션 제목: “낙관적 업데이트”더 나은 사용자 경험을 위해 낙관적 업데이트를 구현하세요:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
function ItemList() { const api = useMyApi(); const queryClient = useQueryClient();
// Query to fetch items const itemsQuery = useQuery(api.listItems.queryOptions());
// Mutation for deleting items with optimistic updates const deleteMutation = useMutation({ ...api.deleteItem.mutationOptions(), onMutate: async (itemId) => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey: api.listItems.queryKey() });
// Snapshot the previous value const previousItems = queryClient.getQueryData(api.listItems.queryKey());
// Optimistically update to the new value queryClient.setQueryData( api.listItems.queryKey(), (old) => old.filter((item) => item.id !== itemId) );
// Return a context object with the snapshot return { previousItems }; }, onError: (err, itemId, context) => { // If the mutation fails, use the context returned from onMutate to roll back queryClient.setQueryData(api.listItems.queryKey(), context.previousItems); console.error('Failed to delete item:', err); }, onSettled: () => { // Always refetch after error or success to ensure data is in sync with server queryClient.invalidateQueries({ queryKey: api.listItems.queryKey() }); }, });
if (itemsQuery.isLoading) { return <LoadingSpinner />; }
if (itemsQuery.isError) { return <ErrorMessage message="Failed to load items" />; }
return ( <ul> {itemsQuery.data.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => deleteMutation.mutate(item.id)} disabled={deleteMutation.isPending} > {deleteMutation.isPending ? 'Deleting...' : 'Delete'} </button> </li> ))} </ul> );}API 클라이언트를 직접 사용한 낙관적 업데이트
function ItemList() { const api = useMyApiClient(); const [items, setItems] = useState([]);
const handleDelete = async (itemId) => { // Optimistically remove the item const previousItems = items; setItems(items.filter((item) => item.id !== itemId));
try { await api.deleteItem(itemId); } catch (error) { // Restore previous items on error setItems(previousItems); console.error('Failed to delete item:', error); } };
return ( <ul> {items.map((item) => ( <li key={item.id}> {item.name} <button onClick={() => handleDelete(item.id)}>Delete</button> </li> ))} </ul> );}타입 안전성
섹션 제목: “타입 안전성”통합은 완전한 엔드 투 엔드 타입 안전성을 제공합니다. IDE는 모든 API 호출에 대해 완전한 자동 완성 및 타입 체크를 제공합니다:
import { useMutation } from '@tanstack/react-query';
function ItemForm() { const api = useMyApi();
// Type-safe mutation for creating items const createItem = useMutation({ ...api.createItem.mutationOptions(), // ✅ Type error if onSuccess callback doesn't handle the correct response type onSuccess: (data) => { // data is fully typed based on your API's response schema console.log(`Item created with ID: ${data.id}`); }, });
const handleSubmit = (data: CreateItemInput) => { // ✅ Type error if input doesn't match schema createItem.mutate(data); };
// Error UI can use type narrowing to handle different error types if (createItem.error) { const error = createItem.error; switch (error.status) { case 400: // error.error is typed as CreateItem400Response return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: // error.error is typed as CreateItem403Response return <AuthError reason={error.error.reason} />; default: // error.error is typed as CreateItem5XXResponse for 500, 502, etc. return <ServerError message={error.error.message} />; } }
return ( <form onSubmit={(e) => { e.preventDefault(); handleSubmit({ name: 'New Item' }); }}> {/* Form fields */} <button type="submit" disabled={createItem.isPending} > {createItem.isPending ? 'Creating...' : 'Create Item'} </button> </form> );}API 클라이언트를 직접 사용한 타입 안전성
function ItemForm() { const api = useMyApiClient(); const [error, setError] = useState<CreateItemError | null>(null);
const handleSubmit = async (data: CreateItemInput) => { try { // ✅ Type error if input doesn't match schema await api.createItem(data); } catch (e) { // ✅ Error type includes all possible error responses const err = e as CreateItemError; switch (err.status) { case 400: // err.error is typed as CreateItem400Response console.error('Validation errors:', err.error.validationErrors); break; case 403: // err.error is typed as CreateItem403Response console.error('Not authorized:', err.error.reason); break; case 500: case 502: // err.error is typed as CreateItem5XXResponse console.error( 'Server error:', err.error.message, 'Trace:', err.error.traceId, ); break; } setError(err); } };
// Error UI can use type narrowing to handle different error types if (error) { switch (error.status) { case 400: return ( <FormError message="Invalid input" errors={error.error.validationErrors} /> ); case 403: return <AuthError reason={error.error.reason} />; default: return <ServerError message={error.error.message} />; } }
return <form onSubmit={handleSubmit}>{/* ... */}</form>;}타입은 FastAPI의 OpenAPI 스키마에서 자동으로 생성되므로 API에 대한 모든 변경 사항이 빌드 후 프론트엔드 코드에 반영됩니다.
사용자 정의 인증
섹션 제목: “사용자 정의 인증”FastAPI가 Custom 인증(Lambda Authorizer)을 사용하는 경우, 생성된 클라이언트 provider를 편집하여 authorizer가 예상하는 인증 헤더를 추가해야 합니다. 생성된 <ApiName>Provider.tsx에서 fetch 구성을 찾아 요청 헤더에 토큰 또는 API 키를 추가하세요.