RESTful 라우팅으로 API 제작

JOOYEUN SEO·2024년 10월 28일

100 Days of Python

목록 보기
66/76
post-thumbnail

❖ REST 방식의 API

REST

  • REpresentatinal State Transfer(표현 상태 전송)의 약자
  • API 디자인에 쓰이는 여러 아키텍쳐 형식 중 하나
    • 웹 API의 표준 아키텍쳐
    • 그 외 SOAP, GraphQL, Falcor 등의 형식 존재
  • 모든 사이트가 공동 규약에 맞춰 똑같은 구조로 API를 구축하자는 아이디어
  • REST 방식의 API 핵심 규칙
    • HTTP verbs(동사, 즉 언어) 사용
      • GET, POST, PUT, PATCH, DELETE 의 5가지 동사
      • 데이터베이스의 CRUD 기능과 비슷
    • 특정한 패턴의 경로 및 엔드포인트 URL 사용

🗂️ Day66 프로젝트: 카페 API

특정 도시의 카페들에 대한 데이터를 수집하여 원격 근무에 적합한 카페를 찾는 API 구축

(런던의 카페 정보가 담긴 db 파일로 실습)

1. HTTP GET - 무작위 카페

🔍 유의 사항

  • 사용자에게 임의의 카페 정보를 제공하는 /random 경로 생성
    • GET 요청은 모든 라우트에서 기본으로 하는 요청
  • /random 경로로 GET 요청을 하면 플라스크 서버가 데이터베이스에서 임의의 카페를 가져옴
    • SQLAlchemy의 random 함수는 파이썬의 함수와 다름
      (SQLAlchemy가 직접 Python 객체의 리스트를 다루는 것이 아니기 때문)
  • render_template()으로 HTML 템플릿을 반환하는 대신 필요한 데이터가 포함된 JSON을 리턴
    • 서버가 API 역할을 하기 때문
    • random_cafe SQL 알케미 객체를 JSON으로 변환해야 함 → 직렬화
      • 플라스크에 직렬화를 돕는 jsonify() 메소드 내장
      • 리턴할 JSON의 구조는 직접 제공해야 한다
  • /random 경로 작동시키기
    • id와 같은 일부 속성을 생략하거나, 일부 속성들을 하위 섹션으로 그룹화하여 반환 가능
    • 데이터베이스 행 객체를 JSON으로 직렬화할 수 있음
      (행 객체를 딕셔너리로 변환한 다음 jsonify() 를 사용하여 JSON으로 변환)

2. HTTP GET - 모든 카페

🔍 유의 사항

  • 모든 카페 목록을 보여주는 /all 경로 생성
  • /all 경로로 GET 요청을 하면 서버가 데이터베이스의 모든 카페를 JSON으로 반환

3. HTTP GET - 카페 찾기

🔍 유의 사항

  • 특정 지역의 모든 카페를 검색하는 /search 경로 생성
  • 위치를 매개변수로 전달하여 /search 경로로 GET 요청
    • 매개변수는 ?와 함께 URL에 전달
    • 매개변수로 올바른 위치를 전달하지 않으면 에러 메시지를 JSON 형식으로 전달하기

4. 포스트맨 - 만능 API 테스트 도구

🔍 유의 사항

  • Postman
    • API 경로 테스트 시 사용하는 도구
      • 매개변수를 브라우저 URL 표시줄에 입력할 필요 없음
      • API 문서를 자동으로 생성
    • 요청 매개변수에 대한 키-값 쌍을 추가할 수 있으며, URL 형식이 자동으로 지정됨
  • Create a Collection 으로 컬렉션 생성 후 Add request로 생성했던 경로들 모두 추가

5. HTTP POST - 새로운 카페

🔍 유의 사항

  • Postman으로 WTForm 또는 HTML 양식을 작성하지 않고 API POST 요청 테스트 가능
    • 요청방식을 POST로 설정 후 /add 경로 붙여넣기
    • Body에서 x-www-form-urlencoded 옵션 선택
    • Postman의 본문 탭에 입력하는 키-값 쌍은 <input> 요소와 동일
      (파이썬 코드에서 request.form.get()에 넣은 이름과 같아야 한다)
  • 파이썬에서 코드 작성 후 실행한 다음 포스트맨에서 POST 요청 테스트

6. HTTP PUT과 PATCH 비교

🔍 유의 사항

  • PUT
    • 기존 정보의 전체를 새 것으로 교체하여 데이터베이스를 업데이트
    • 🚲 앞바퀴만 망가진 자전거를 아예 새 자전거로 교체하는 것과 같음
  • PATCH
    • 데이터 일부만 새로 업데이트
    • 🛞 망가진 앞바퀴 부품만 새로 교체하는 것과 같음

7. HTTP PATCH - 카페의 커피가격

🔍 유의 사항

  • 사용자가 어느 카페의 블랙커피 가격 변동 사항을 제출하려면
    • GET 요청으로 해당 카페의 id를 알아낸 뒤,
    • coffee_price 필드를 업데이트할 수 있음
    • 나머지 데이터는 변경할 필요 없기 때문에 PUT 보다 PATCH 요청이 효율적
  • PATCH 요청 경로 생성
  • API가 RESTful이 되기 위한 이상적인 경로 : /update-price/<cafe_id>
    • 요청을 매개변수로 전달하여 업데이트된 블랙 커피 한 잔 가격을 제공해야 한다
  • 경로에 id가 존재하지 않을 경우 올바른 피드백 제공하기

id가 22인 카페의 커피 가격이 업데이트됨


8. HTTP DELETE - 폐쇄된 카페

🔍 유의 사항

  • 폐업한 카페가 있을 경우 사용자가 DELETE 요청으로 데이터베이스 업데이트
  • 아무나 데이터베이스에서 항목을 삭제하지 못하도록 보안 기능 추가하기
    • TopSecretAPIKey 라는 API 키 값이 있으면 삭제 요청이 가능
    • 키가 없으면 해당 요청을 할 권한이 없다고 알려주기
  • DELETE 요청 경로를 /report-closed/<cafe_id> 에 추가
  • 사용자가 잘못된 api키값을 갖고 있거나 해당 id의 카페가 존재하지 않을 경우 각각 다른 응답 필요

9. API 문서 작성하기

🔍 유의 사항

  • Postman에서 API 사용 방법을 문서화하기
  • 해당 콜렉션View documentationPublish → 생성된 링크 이용
  • 링크에서 언어를 Python - Requests 로 변경해서 코드 보기
  • index.html에서 앵커 태그로 API 문서 링크 포함시키기

⌨️ main.py

from flask import Flask, jsonify, render_template, request
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, Boolean
from sqlalchemy.sql.functions import random

app = Flask(__name__)

# CREATE DB
class Base(DeclarativeBase):
    pass
# Connect to Database
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///cafes.db'
db = SQLAlchemy(model_class=Base)
db.init_app(app)


# Cafe TABLE Configuration
class Cafe(db.Model):
    id: Mapped[int] = mapped_column(Integer, primary_key=True)
    name: Mapped[str] = mapped_column(String(250), unique=True, nullable=False)
    map_url: Mapped[str] = mapped_column(String(500), nullable=False)
    img_url: Mapped[str] = mapped_column(String(500), nullable=False)
    location: Mapped[str] = mapped_column(String(250), nullable=False)
    seats: Mapped[str] = mapped_column(String(250), nullable=False)
    has_toilet: Mapped[bool] = mapped_column(Boolean, nullable=False)
    has_wifi: Mapped[bool] = mapped_column(Boolean, nullable=False)
    has_sockets: Mapped[bool] = mapped_column(Boolean, nullable=False)
    can_take_calls: Mapped[bool] = mapped_column(Boolean, nullable=False)
    coffee_price: Mapped[str] = mapped_column(String(250), nullable=True)

    # 키(열 이름)와 값(해당 열의 값들) 쌍의 딕셔너리를 생성하는 함수
    def to_dict(self):
        # getattr(객체이름, 속성이름) : 해당 객체의 특정 속성 값을 동적으로 가져옴
        return {column.name: getattr(self, column.name) for column in self.__table__.columns}


with app.app_context():
    db.create_all()

@app.route("/")
def home():
    return render_template("index.html")


# HTTP GET - Read Record
@app.route("/random")
def get_random_cafe():
    cafes = db.session.query(Cafe).all()
    # Cafe 테이블에서 임의의 레코드 하나 선택(데이터베이스에서 쿼리를 수행하는 방식)
    random_cafe = db.session.query(Cafe).order_by(random()).first()
    # jsonify() : Flask에서 리스트나 딕셔너리 형 데이터를 JSON 형식으로 변환하는 함수
    return jsonify(cafe=random_cafe.to_dict())

@app.route("/all")
def get_all_cafes():
    cafes = db.session.query(Cafe).all()
    return jsonify(cafes=[cafe.to_dict() for cafe in cafes])

@app.route("/search")
def get_cafe_at_location():
    query_location = request.args.get("loc")
    cafes = db.session.query(Cafe).filter_by(location=query_location).all()
    if cafes:
        return jsonify(cafes=[cafe.to_dict() for cafe in cafes])
    else:
        return jsonify(error={"Not Found": "Sorry, we don't have a cafe at that location."})


# HTTP POST - Create Record
@app.route("/add", methods=["GET", "POST"])
def post_new_cafe():
    new_cafe = Cafe(
        name=request.form.get("name"),
        map_url=request.form.get("map_url"),
        img_url=request.form.get("img_url"),
        location=request.form.get("loc"),
        has_sockets=bool(int(request.form.get("sockets"))),
        has_toilet=bool(int(request.form.get("toilet"))),
        has_wifi=bool(int(request.form.get("wifi"))),
        can_take_calls=bool(int(request.form.get("calls"))),
        seats=request.form.get("seats"),
        coffee_price=request.form.get("coffee_price"),
    )
    db.session.add(new_cafe)
    db.session.commit()
    return jsonify(response={"success": "Successfully added the new cafe."})


# HTTP PUT/PATCH - Update Record
@app.route("/update-price/<int:cafe_id>", methods=["GET", "PATCH"])
def patch_new_price(cafe_id):
    new_price = request.args.get("new_price")
    cafe = db.session.query(Cafe).get(cafe_id)
    if cafe:
        cafe.coffee_price = new_price
        db.session.commit()
        return jsonify(response={"success": "Successfully updated the price."}), 200
    else:
        return jsonify(error={"Not Found": "Sorry a cafe with that id was not found in the database."}), 404


# HTTP DELETE - Delete Record
@app.route("/report-closed/<cafe_id>", methods=["GET", "DELETE"])
def delete_closed_cafe(cafe_id):
    api_key = request.args.get("api-key")
    if api_key == "TopSecretAPIKey":
        cafe = db.session.query(Cafe).get(cafe_id)
        if cafe:
            db.session.delete(cafe)
            db.session.commit()
            return jsonify(response={"success": "Successfully deleted the cafe from the database."}), 200
        else:
            return jsonify(error={"Not Found": "Sorry a cafe with that id was not found in the database."}), 404
    else:
        return jsonify(error={"Forbidden": "Sorry, that's not allowed. Make sure you have the correct api_key."}), 403


if __name__ == '__main__':
    app.run(debug=True)

🏗️ index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Cafe&Wifi API</title>
</head>
<body>
    <h1>Welcome to the Cafe & Wifi API</h1>
<a href="https://documenter.getpostman.com/view/39314524/2sAY4uC3fz">Read the Documentation</a>
</body>
</html>




▷ Angela Yu, [Python 부트캠프 : 100개의 프로젝트로 Python 개발 완전 정복], Udemy, https://www.udemy.com/course/best-100-days-python/?couponCode=ST3MT72524

0개의 댓글