개발/FastAPI

FastAPI CRUD API 만들기: GET, POST, PUT, DELETE 한 번에 이해하기

notebase 2026. 5. 27. 08:52

FastAPI로 간단한 CRUD API를 만드는 방법을 예제로 정리했습니다. GET, POST, PUT, DELETE 요청이 각각 어떤 역할을 하는지 코드와 함께 확인할 수 있습니다.

 

FastAPI CRUD API를 처음 만들 때는 데이터베이스부터 연결하기보다, 메모리 데이터를 사용해 흐름을 먼저 잡는 게 좋습니다. GET, POST, PUT, DELETE가 각각 어떤 역할을 하는지 코드로 바로 확인할 수 있기 때문입니다.

이번 예제에서는 상품 정보를 다루는 간단한 CRUD API를 만들어보겠습니다.

데이터베이스는 아직 사용하지 않습니다.
파이썬 딕셔너리에 데이터를 저장하는 방식으로 API 구조만 먼저 익힙니다.


 

CRUD API란?

CRUD는 데이터를 다룰 때 가장 기본이 되는 4가지 작업입니다.

CRUD 의미 HTTP 메서드 예시
Create 생성 POST 상품 추가
Read 조회 GET 상품 목록 조회
Update 수정 PUT 상품 정보 수정
Delete 삭제 DELETE 상품 삭제

 

실제로 백엔드 API를 만들면 대부분 이 흐름에서 시작합니다.

예를 들어 쇼핑몰 상품 API라면 다음과 같은 기능이 필요합니다.

  • 상품 목록 조회
  • 특정 상품 조회
  • 새 상품 추가
  • 상품 이름이나 가격 수정
  • 상품 삭제

이게 바로 CRUD API입니다.


 

만들 API 구조

이번 글에서 만들 API는 아래와 같습니다.

기능 메서드 경로
전체 상품 조회 GET /items
특정 상품 조회 GET /items/{item_id}
상품 생성 POST /items
상품 수정 PUT /items/{item_id}
상품 삭제 DELETE /items/{item_id}

 

여기서 {item_id}는 경로 파라미터입니다.

예를 들어 /items/1로 요청하면 ID가 1인 상품을 조회합니다.


 

프로젝트 준비

먼저 프로젝트 폴더를 만들고 이동합니다.

mkdir fastapi-crud-example
cd fastapi-crud-example

 

가능하면 프로젝트별로 가상환경을 만든 뒤 진행하는 것을 추천합니다.
여러 파이썬 프로젝트를 같이 다루다 보면 패키지 버전이 섞여 문제가 생길 수 있기 때문입니다.

macOS 또는 Linux에서는 아래처럼 실행합니다.

python -m venv .venv
source .venv/bin/activate

 

Windows에서는 아래 명령어를 사용할 수 있습니다.

python -m venv .venv
.venv\Scripts\activate

 

그다음 FastAPI와 실행 서버인 Uvicorn을 설치합니다.

pip install fastapi uvicorn

 

프로젝트 폴더 안에 main.py 파일을 만듭니다.

fastapi-crud-example/
└── main.py

 

FastAPI는 파이썬 타입 힌트를 기반으로 API를 만들 수 있는 웹 프레임워크입니다.
입문 단계에서는 타입 힌트가 낯설 수 있지만, FastAPI에서는 이 타입 정보가 요청 데이터 검증과 자동 문서화에 같이 활용됩니다.


 

기본 코드 작성하기

main.py에 먼저 기본 구조를 작성합니다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()

 

여기서 FastAPI()는 애플리케이션 객체입니다.

HTTPException은 요청한 데이터가 없거나 잘못된 요청이 들어왔을 때 HTTP 상태 코드를 반환하기 위해 사용합니다.

BaseModel은 요청 데이터의 구조를 정의할 때 사용합니다.
FastAPI에서는 요청 본문을 받을 때 Pydantic 모델을 사용해 데이터 구조를 선언할 수 있습니다.


 

상품 모델 만들기

이번 예제에서는 상품에 name, price, description 값을 넣겠습니다.

class Item(BaseModel):
    name: str
    price: int
    description: str | None = None

 

이 예제는 Python 3.10 이상 문법을 기준으로 작성했습니다.
str | None은 “문자열이거나 None일 수 있다”는 뜻입니다.

Python 3.9 이하를 사용한다면 아래처럼 Optional을 사용하면 됩니다.

from typing import Optional

class Item(BaseModel):
    name: str
    price: int
    description: Optional[str] = None

 

각 필드의 의미는 다음과 같습니다.

필드 타입 설명
name str 상품 이름
price int 상품 가격
description str | None 상품 설명, 없어도 됨

 

description에는 기본값으로 None을 넣었습니다.

즉, 상품을 생성할 때 설명은 생략할 수 있습니다.

{
  "name": "Keyboard",
  "price": 50000
}

 

이런 요청도 가능합니다.


 

임시 데이터 저장소 만들기

아직 데이터베이스를 연결하지 않으므로 파이썬 딕셔너리를 사용합니다.

items = {
    1: {
        "name": "Keyboard",
        "price": 50000,
        "description": "Mechanical keyboard"
    },
    2: {
        "name": "Mouse",
        "price": 30000,
        "description": "Wireless mouse"
    }
}

 

실제 서비스라면 이 데이터는 서버가 꺼질 때 사라지면 안 됩니다.

그래서 SQLite, PostgreSQL 같은 데이터베이스를 사용합니다.
하지만 지금 단계에서는 CRUD 흐름을 이해하는 게 목적이므로 메모리 데이터로 충분합니다.

여기서 items 딕셔너리의 키는 1, 2처럼 정수입니다.
뒤에서 API 응답을 보면 "1", "2"처럼 문자열로 보이는데, 이건 오류가 아닙니다.

JSON 객체의 키는 문자열로 표현되기 때문에 FastAPI가 응답을 JSON으로 변환하는 과정에서 그렇게 표시됩니다.
파이썬 코드 안에서는 여전히 items[1]처럼 정수 키로 접근합니다.


 

전체 상품 조회 API 만들기

먼저 전체 상품을 조회하는 API입니다.

@app.get("/items")
def get_items():
    return items

 

이제 서버를 실행합니다.

uvicorn main:app --reload

 

브라우저에서 아래 주소로 접속합니다.

http://127.0.0.1:8000/items

 

그러면 현재 저장된 상품 목록이 JSON 형태로 반환됩니다.

{
  "1": {
    "name": "Keyboard",
    "price": 50000,
    "description": "Mechanical keyboard"
  },
  "2": {
    "name": "Mouse",
    "price": 30000,
    "description": "Wireless mouse"
  }
}

 

여기서 중요한 점은 API의 응답이 HTML 페이지가 아니라 JSON 데이터라는 점입니다.

백엔드 API는 보통 프론트엔드, 모바일 앱, 다른 서버가 데이터를 가져갈 수 있도록 JSON을 반환합니다.


 

특정 상품 조회 API 만들기

이번에는 상품 ID를 받아서 하나의 상품만 조회하는 API를 만듭니다.

@app.get("/items/{item_id}")
def get_item(item_id: int):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")

    return items[item_id]

 

요청 예시는 다음과 같습니다.

GET /items/1

 

응답은 이렇게 나옵니다.

{
  "name": "Keyboard",
  "price": 50000,
  "description": "Mechanical keyboard"
}

 

여기서 /items/{item_id}{item_id}는 함수의 item_id 매개변수로 전달됩니다.

주소로 들어오는 값은 원래 문자열입니다.
하지만 함수에서 item_id: int로 선언했기 때문에 FastAPI가 정수로 변환해 줍니다.

예를 들어 /items/1로 요청하면 "1"이라는 문자열이 들어오지만, 함수 안에서는 정수 1로 다룰 수 있습니다.
만약 /items/abc처럼 숫자로 바꿀 수 없는 값이 들어오면 FastAPI가 자동으로 검증 오류를 반환합니다.

존재하지 않는 ID를 요청하면 404 Not Found를 반환합니다.

GET /items/999

 

응답 예시는 다음과 같습니다.

{
  "detail": "Item not found"
}

 

여기서 단순히 "없음" 같은 문자열을 반환하지 않고 404 상태 코드를 반환하는 게 좋습니다.

API를 사용하는 쪽에서 “요청은 성공했지만 데이터가 없는 것인지”, “요청한 리소스 자체가 없는 것인지”를 구분할 수 있기 때문입니다.


 

상품 생성 API 만들기

상품을 새로 추가할 때는 POST를 사용합니다.

@app.post("/items")
def create_item(item: Item):
    new_id = max(items.keys()) + 1 if items else 1

    items[new_id] = item.model_dump()

    return {
        "id": new_id,
        "item": items[new_id]
    }

 

item: Item 부분이 요청 본문입니다.

클라이언트가 아래와 같은 JSON을 보내면 FastAPI가 Item 모델에 맞는지 자동으로 확인합니다.

{
  "name": "Monitor",
  "price": 200000,
  "description": "27 inch monitor"
}

 

정상 요청이면 새 ID와 함께 생성된 상품이 반환됩니다.

{
  "id": 3,
  "item": {
    "name": "Monitor",
    "price": 200000,
    "description": "27 inch monitor"
  }
}

 

item.model_dump()는 Pydantic 모델을 파이썬 딕셔너리로 바꾸는 코드입니다.

Pydantic v2 기준에서는 예전의 .dict()보다 .model_dump()를 사용하는 방식이 더 자연스럽습니다.


 

상품 수정 API 만들기

기존 상품을 수정할 때는 PUT을 사용합니다.

@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")

    items[item_id] = item.model_dump()

    return {
        "id": item_id,
        "item": items[item_id]
    }

 

이 코드에서도 /items/{item_id}{item_id}가 함수의 item_id: int로 연결됩니다.
특정 상품 조회 API와 같은 방식으로 FastAPI가 경로 값을 정수로 변환하고 검증합니다.

요청 예시는 다음과 같습니다.

PUT /items/1

 

요청 본문은 이렇게 보낼 수 있습니다.

{
  "name": "Gaming Keyboard",
  "price": 80000,
  "description": "RGB mechanical keyboard"
}

 

그러면 ID가 1인 상품 정보가 새 데이터로 바뀝니다.

{
  "id": 1,
  "item": {
    "name": "Gaming Keyboard",
    "price": 80000,
    "description": "RGB mechanical keyboard"
  }
}

 

여기서 오해하기 쉬운 부분이 있습니다.

PUT은 보통 “전체 수정”에 가깝게 사용합니다.
즉, 상품 이름만 바꾸고 싶더라도 전체 필드를 다시 보내는 방식입니다.

일부 필드만 수정하려면 PATCH를 따로 설계하는 경우가 많습니다.

이번 글에서는 CRUD 기본 구조에 집중하기 위해 PUT만 사용하겠습니다.


 

상품 삭제 API 만들기

상품을 삭제할 때는 DELETE를 사용합니다.

@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")

    deleted_item = items.pop(item_id)

    return {
        "message": "Item deleted",
        "item": deleted_item
    }

 

요청 예시는 다음과 같습니다.

DELETE /items/2

 

응답은 이렇게 나옵니다.

{
  "message": "Item deleted",
  "item": {
    "name": "Mouse",
    "price": 30000,
    "description": "Wireless mouse"
  }
}

 

삭제 후 다시 /items를 조회하면 ID가 2인 상품은 사라진 상태입니다.

단, 지금은 메모리 데이터를 사용하므로 서버를 재시작하면 처음 코드에 적어둔 데이터로 다시 돌아갑니다.


 

전체 코드

아래는 지금까지 작성한 전체 코드입니다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: int
    description: str | None = None


items = {
    1: {
        "name": "Keyboard",
        "price": 50000,
        "description": "Mechanical keyboard"
    },
    2: {
        "name": "Mouse",
        "price": 30000,
        "description": "Wireless mouse"
    }
}


@app.get("/items")
def get_items():
    return items


@app.get("/items/{item_id}")
def get_item(item_id: int):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")

    return items[item_id]


@app.post("/items")
def create_item(item: Item):
    new_id = max(items.keys()) + 1 if items else 1

    items[new_id] = item.model_dump()

    return {
        "id": new_id,
        "item": items[new_id]
    }


@app.put("/items/{item_id}")
def update_item(item_id: int, item: Item):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")

    items[item_id] = item.model_dump()

    return {
        "id": item_id,
        "item": items[item_id]
    }


@app.delete("/items/{item_id}")
def delete_item(item_id: int):
    if item_id not in items:
        raise HTTPException(status_code=404, detail="Item not found")

    deleted_item = items.pop(item_id)

    return {
        "message": "Item deleted",
        "item": deleted_item
    }

 

서버 실행 명령어는 다음과 같습니다.

uvicorn main:app --reload

 


 

Swagger UI에서 테스트하기

FastAPI의 편한 점 중 하나는 API 문서가 자동으로 생성된다는 점입니다.

서버를 실행한 뒤 아래 주소로 접속합니다.

http://127.0.0.1:8000/docs

 

그러면 Swagger UI 화면이 열립니다.

여기서 직접 GET, POST, PUT, DELETE 요청을 테스트할 수 있습니다.

초보자 입장에서는 Postman 같은 별도 도구 없이도 API 요청을 바로 테스트할 수 있어서 실습하기 좋습니다.


 

테스트 순서 추천

처음 실습할 때는 아래 순서대로 테스트해 보면 흐름이 잘 보입니다.

  1. GET /items로 전체 상품 조회
  2. GET /items/1로 특정 상품 조회
  3. POST /items로 새 상품 생성
  4. 다시 GET /items로 상품이 추가됐는지 확인
  5. PUT /items/1로 상품 수정
  6. DELETE /items/2로 상품 삭제
  7. 다시 GET /items로 삭제 결과 확인

이 순서대로 해보면 CRUD가 단순히 코드 조각이 아니라 하나의 데이터 흐름이라는 점이 보입니다.


 

이 예제에서 주의할 점

이번 예제는 CRUD API 구조를 이해하기 위한 입문용 코드입니다.

실제 서비스 코드와는 차이가 있습니다.

첫째, 데이터가 메모리에 저장됩니다.
서버를 재시작하면 데이터가 초기화됩니다.

둘째, ID 생성 방식이 단순합니다.
max(items.keys()) + 1 방식은 혼자 실습할 때는 괜찮지만, 여러 요청이 동시에 들어오는 실제 서비스에서는 적절하지 않습니다.

셋째, 인증과 권한 검사가 없습니다.
실제 서비스에서는 아무나 상품을 생성하거나 삭제하면 안 됩니다.

넷째, 데이터베이스 연결이 없습니다.
운영 환경에서는 SQLite, PostgreSQL, MySQL 같은 데이터베이스와 연결해야 합니다.

그래도 입문 단계에서는 이 방식이 더 낫습니다.
처음부터 데이터베이스, 세션, ORM까지 같이 넣으면 CRUD의 핵심 흐름이 흐려질 수 있기 때문입니다.


 

마무리

FastAPI CRUD API의 기본 흐름은 복잡하지 않습니다.

  • GET으로 조회한다
  • POST로 생성한다
  • PUT으로 수정한다
  • DELETE로 삭제한다

이 네 가지 구조만 제대로 이해해도 API 설계의 기본 뼈대는 잡을 수 있습니다.

다음 단계로 넘어간다면 같은 예제를 SQLite나 SQLModel과 연결해 보는 것이 좋습니다.
그때부터는 서버를 재시작해도 데이터가 유지되고, 실제 백엔드 프로젝트에 가까운 구조로 확장할 수 있습니다.