Tutorial RESTful API dengan FastAPI Python + MongoDB Part 1 — Instalasi dan CRUD
Belajar membuat RESTful API dengan salah satu framework Python tercepat saat ini!
Halo semua, Kiddy disini dan pada kesempatan kali ini saya ingin berbagi insight mengenai cara membuat RESTful API dengan FastAPI Python menggunakan Database MongoDB dan kita akan melengkapinya dengan unit test.
Sebelum kalian melanjutkan tutorial ini, saya anggap kalian sudah paham dasar-dasar Python seperti dasar API dengan Flask, MongoDB dan hal-hal printilan lainnya. Kebingungan akibat isi tutorial ini bukan saya yang tanggung jawab ya xixixi.
Ngomongin soal RESTful API, ada satu Framework yang lagi panas-panasnya dibicarain pada kalangan developer Python. Nah apasih framework ini? Yes, FastAPI.
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
FastAPI adalah web framework API dengan Python 3.6 yang berbasiskan standar Python, dan diklaim performanya cepat, dan penulisannya modern.
Nah apa aja sih fitur di FastAPI?
- Cepat, Ini framework Python cepet banget sampe dikomparasi sama NodeJS dan Go. Digabung dengan 2 framework Python yaitu Starlette dan Pydantic, FastAPI jadi salah satu Fraemwork Python tercepat saat ini (Good bye Flask! 😣)
- Cepat untuk development, FastAPI klaim bahwa mereka bisa ningkatin kecepatan coding Developer hingga 200 sampai 300%, gila bener.
- Sedikit bugs, FastAPI klaim bahwa framework ini lebih sedikit bug yang dihasilkan.
- Intuitif, kode otomatis komplit yang tersedia dimana aja (VSCode mungkin?), dan waktu untuk debugging jadi lebih sikit (katanya).
- Gampang, Didesain emang buat gampang dipahami guys, jadi bener-bener bentar baca docs aja langsung paham.
- Serba pendek, meminimalisir kode duplikat dan juga bisa pake fitur dari deklarasi di class loh guys~
- Kuat, udah siap banget untuk production loh, bahkan ada dokumen otomatisnya loh! xixixi
- Berbasis standar, dokumentasinya udah berbasis open standar (OpenAPI) yang dulunya dikenal dengan nama Swagger.
Ohiya, bagi yang sebelumnya pernah make javascript, make FastAPI bakalan shock karena ternyata Python bisa dibuat Asynchronous loh (HAH SERIUS?), iya beneran gue ngga bohong, disini, kita bisa ngebuat Asynchronous karena emang FastAPI itu sifatnya async, jadi siap-siap aja ketemu await-await kalo butuh Synchronous hahahaha.
Oke saatnya kita mulai coding. Kita buat nama projeknya learnfastapi ya. Jangan sampe buat folder projek namanya fastapi karena bisa bikin kamu error pas lagi compiling, soalnya nama projeknya sama nama library env-nya sama :(
Jangan lupa dibuat virtualenv-nya, saya dah ngga terangin lagi ya, masuk kesini saya anggap udah bisa.
Ohiya, saya menggunakan Python 3.8 ya, jangan lupa juga temen-temen install MongoDB terlebih dahulu, caranya ya browsing sendiri~
Oke sekarang kita install dulu yang kita butuhkan:
pip install fastapi uvicorn mongoengine pytest wheel mongomock
Kalo udah saatnya kita mulai ngoding, kita buat dulu struktur foldernya seperti ini ya:
Sekarang kita akan mulai masuk ke __init__.pynya dulu.
from fastapi import FastAPI
from mongoengine import connect
from starlette.testclient import TestClient
from app.routers import users, todos
app = FastAPI()
mongodb = connect('mongodb', host='mongodb://localhost/test_db')
client = TestClient(app)
app.include_router(users.router, prefix="/users", tags=["User Docs"], )
app.include_router(todos.router, prefix="/todo", tags=["Todo Docs!"], )
Kalo ada merah-merah diemin aja dulu.
Sekarang kita buka folder models, dan kita buat model bernama user.py dan todo.py, model ini akan digunakan oleh MongoEngine karena kita menggunakan ODM (Object Document Mapper), sahabar barunya ORM loh gan~
models/user.py:
from mongoengine import *
class Users(Document):
name = StringField(max_length=200, required=True)
models/todo.py:
from mongoengine import *
from app.models.user import Users
class Todos(Document):
title = StringField(max_length=200, required=True)
description = StringField()
owner = LazyReferenceField(Users, reverse_delete_rule=CASCADE)
Oke saya jelasin dikit, strukturnya bisa dibilang hampir mirip sama FlaskSQL-Alchemy, bagi yang pernah belajar Flask di tutorial saya pasti ngga akan asing deh. Nah kita menggunakan LazyReferenceField di bagian owner, ini sama saja seperti relation foreign key di MySQL, dan kerennya adalah MongoDB ini support banyak banget tipe Document, kaian bisa cek sendiri seberapa kerennya tipe-tipe document yang ada di Mongo Engine (https://docs.mongoengine.org/guide/defining-documents.html).
Nah reverse_delete_rule yang ada di attribut owner, menandakan kalo owner alias si Usernya dihapus datanya, object owner itu juga akan dihapus pada document Todos, konsepnya sama seperti On delete cascadenya MySQL.
Nah lanjut, sekarang kita buat controllernya.
controllers/UserController.py
from starlette.requests import Request
from starlette.responses import JSONResponse
from app import response
from app.models.user import Users
from app.transformers import UserTransformer
class UserController:
@staticmethod
async def index(request: Request) -> JSONResponse:
try:
users = Users.objects()
transformer = UserTransformer.transform(users)
return response.ok(transformer, "")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def store(request: Request) -> JSONResponse:
try:
body = await request.json()
name = body['name']
if name == "":
raise Exception("name couldn't be empty!")
user = Users(name=name)
user.save()
transformer = UserTransformer.singleTransform(user)
return response.ok(transformer, "Berhasil Membuat User!")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def show(id) -> JSONResponse:
try:
user = Users.objects(id=id).first()
if user is None:
raise Exception('user tidak ditemukan!')
transformer = UserTransformer.singleTransform(user)
return response.ok(transformer, "")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def update(id: str, request: Request) -> JSONResponse:
try:
body = await request.json()
name = body['name']
if name == "":
raise Exception("name couldn't be empty!")
user = Users.objects(id=id).first()
if user is None:
raise Exception('user tidak ditemukan!')
user.name = name
user.save()
transformer = UserTransformer.singleTransform(user)
return response.ok(transformer, "Berhasil Mengubah User!")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def delete(id: str) -> JSONResponse:
try:
user = Users.objects(id=id).first()
if user is None:
raise Exception('user tidak ditemukan!')
user.delete()
return response.ok('', "Berhasil Menghapus User!")
except Exception as e:
return response.badRequest('', f'{e}')
Oke, kalo diperhatikan ada yang agak unik dari cara ngoding saya, yaitu ketika membuat method, kita terdapat parameter + attribut yang wajib dikirim selain itu disini juga menjelaskan bahwa kembalian dari si method tersebut adalah JSONResponse. Lucu ya ada async dan awaitnya gitu, serasa ngoding Javascript tapi di Python hahaha.
controllers/TodoController.py
from starlette.requests import Request
from starlette.responses import JSONResponse
from app import response
from app.models.todo import Todos
from app.transformers import TodoTransformer
class TodoController:
@staticmethod
async def index(request: Request) -> JSONResponse:
try:
todos = Todos.objects.all()
todos = TodoTransformer.transform(todos)
return response.ok(todos, '')
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def store(request: Request) -> JSONResponse:
try:
body = await request.json()
title = body['title']
user_id = body['user_id']
todo = Todos()
todo.title = title
todo.owner = user_id
todo.save()
todos = TodoTransformer.singleTransform(todo)
return response.ok(todos, "Berhasil Membuat Todo!")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def show(id) -> JSONResponse:
try:
todo = Todos.objects(id=id).first()
if todo is None:
raise Exception('todo tidak ditemukan!')
todos = TodoTransformer.singleTransform(todo)
return response.ok(todos, "")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def update(id: str, request: Request) -> JSONResponse:
try:
body = await request.json()
title = body['title']
description = body['description']
todo = Todos.objects(id=id).first()
if todo is None:
raise Exception('todo tidak ditemukan!')
todo.title = title
todo.description = description
todo.save()
todos = TodoTransformer.singleTransform(todo)
return response.ok(todos, "Berhasil Mengubah Todo!")
except Exception as e:
return response.badRequest('', f'{e}')
@staticmethod
async def delete(id: str) -> JSONResponse:
try:
todo = Todos.objects(id=id).first()
if todo is None:
raise Exception('todo tidak ditemukan!')
todo.delete()
return response.ok('', "Berhasil menghapusTodo!")
except Exception as e:
return response.badRequest('', f'{e}')
Sekarang, kita buat transformer dulu, karena JSON response tidak bisa menerima response dalam bentuk Object yang akan menyebabkan JSON Error Serializable, maka kita akan mapping ke array terlebih dahulu, mapping ini juga memudahkan agan-agan kalau ingin memodify isi array sebelum dikirimkan dalam bentuk JSON, biasanya saya menggabungkannya dengan kondisi-kondisi, seperti kalo misalnya user ini cocok dengan token, dikembalikan nilai true, dan sebagainya. Yuk cuss buat.
transformers/UserTransformer.py
def transform(items):
array = []
for item in items:
array.append(singleTransform(item))
return array
def singleTransform(values):
return {
"id": str(values.id),
"name": values.name,
}
transformers/TodoTransformer.py
from app.transformers import UserTransformer
def transform(items):
array = []
for item in items:
array.append(singleTransform(item))
return array
def singleTransform(values):
return {
"id": str(values.id),
"title": str(values.title),
"description": str(values.description) if values.description else "",
"owner_id": str(values.owner.id) if values.owner else "",
"owner": UserTransformer.singleTransform(values.owner.fetch()) if values.owner else {}
}
Kenapa ada values.owner.fetch pada bagian owner? Karena kita menggunakan LazyReferenceField. LazyReferenceField ini membuat mongoengine tidak langsung merefer value si owner, melainkan wajib kita fetch dulu, hubungannya sama performa gan! Kalo ngga perlu itemnya untuk direfer dalam setiap request ya ada baiknya pake LazyReference ajah.
Nah kenapa owner_id tidak perlu di fetch terlebih dahulu? Karena sebenernya kita kan hanya menyimpan ObjectID dari si user, maka kita bisa dapatkan IDnya bahkan tanpa fetch() terlebih dahulu.
Seperti yang kita lihat, di bagian owner kita hanya memasukkan ObjectID dari id user yang ada di table users, jadi even tanpa fetching pun, kita udah bisa dapetin apa IDnya.
Sekarang buka folder routers kalian, kita akan membuat dua router alias routing, dimana router ini mirip konsep blueprint di Flask, jadi kita bisa bebas dengan mudah membuat service app dengan organisir ala kita di project FastAPI.
routers/users.py
from fastapi import APIRouter
from starlette.requests import Request
from app.controllers.UserController import UserController as controller
router = APIRouter()
@router.get("", tags=["users"])
async def action(request: Request):
return await controller.index(request)
@router.get("/{id}", tags=["users"])
async def action(id: str):
return await controller.show(id)
@router.post("", tags=["users"])
async def action(request: Request):
return await controller.store(request)
@router.put("/{id}", tags=["users"])
async def action(id: str, request: Request):
return await controller.update(id, request)
@router.delete("/{id}", tags=["users"])
async def action(id: str):
return await controller.delete(id)
routers/todos.py
from fastapi import APIRouter
from starlette.requests import Request
from app.controllers.TodoController import TodoController as controller
router = APIRouter()
@router.get("", tags=["todos"])
async def action(request: Request):
return await controller.index(request)
@router.get("/{id}", tags=["todos"])
async def action(id: str):
return await controller.show(id)
@router.post("", tags=["todos"])
async def action(request: Request):
return await controller.store(request)
@router.put("/{id}", tags=["todos"])
async def action(id: str, request: Request):
return await controller.update(id, request)
@router.delete("/{id}", tags=["todos"])
async def action(id: str):
return await controller.delete(id)
Emang agak ribet keliatannya dengan adanya router, tapi sebenernya mempermudah loh, kalopun agan harus melakukan sesuatu sebelum controller dipanggil, agan bisa melakukan sesuatu disini hehehe, jadi keren banget sebenernya~
Fungsi await juga dibutuhkan untuk ditulis karena controller kita sifatnya async maka harus dipanggil dengan await, kalo nggak ya error. 😢
Terakhir, kita buat file response.py sejajar dengan __init__.py kita, isinya adalah bentuk response dasar yang udah kita refactor.
app/response.py
from starlette import status
from starlette.responses import JSONResponse
def ok(values, message):
return JSONResponse(status_code=status.HTTP_200_OK, content={"values": values, "message": message})
def badRequest(values, message):
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST, content={"values": values, "message": message})
Sekarang di main.py kita akan membuat file dasarnya, yaitu file yang pertama kali dipanggil saat file main.py dijalankan.
main.py
import uvicorn
from app import app
if __name__ == '__main__':
uvicorn.run(app)
Kalo misalnya kalian ngga suka jalanin dari main.py langsung, bisa banget ngejalaninnya pake cara:
uvicorn main:app --reload --port 5000 --log-level=debug
dia bakalan mirip sama flask yang otomatis reload kalo ada code yang berubah!
Oke sekarang kita gasken cek ombak dulu.
Oke sekarang kita cek ombak untuk membuat Todo, nanti saya kasih hal yang menarik, disini saya membuat Todo menggunakan user id milik saya yaitu Kiddy.
Saya membuat Todo list menggunakan Kiddy, sekarang saya juga akan buat Todo menggunakan user Ibie dan user Hudya.
Sekarang isi Get all Todo saya sudah banyak~
{
"values": [
{
"id": "5f8c0ff7b5b9ef536990e6e7",
"title": "Beli semen 3kg",
"description": "",
"owner_id": "5f8c0eedb5b9ef536990e6e5",
"owner": {
"id": "5f8c0eedb5b9ef536990e6e5",
"name": "Kiddy"
}
},
{
"id": "5f8c10b7b5b9ef536990e6e8",
"title": "Beli makanan kucing",
"description": "",
"owner_id": "5f8c0eedb5b9ef536990e6e5",
"owner": {
"id": "5f8c0eedb5b9ef536990e6e5",
"name": "Kiddy"
}
},
{
"id": "5f8c10deb5b9ef536990e6e9",
"title": "Mengerjakan tugas kuliah",
"description": "",
"owner_id": "5f8c0ef0b5b9ef536990e6e6",
"owner": {
"id": "5f8c0ef0b5b9ef536990e6e6",
"name": "Ibie"
}
},
{
"id": "5f8c10f9b5b9ef536990e6ea",
"title": "Cuci Motor",
"description": "",
"owner_id": "5f8c0ea2b5b9ef536990e6e4",
"owner": {
"id": "5f8c0ea2b5b9ef536990e6e4",
"name": "Hudya"
}
}
],
"message": ""
}
Sekarang saya akan ubah salah satu Todo List si Hudya.
Dan sekarang todo list si Hudya saya hapus.
Nah kalau kita get lagi.
{
"values": [
{
"id": "5f8c0ff7b5b9ef536990e6e7",
"title": "Beli semen 3kg",
"description": "",
"owner_id": "5f8c0eedb5b9ef536990e6e5",
"owner": {
"id": "5f8c0eedb5b9ef536990e6e5",
"name": "Kiddy"
}
},
{
"id": "5f8c10b7b5b9ef536990e6e8",
"title": "Beli makanan kucing",
"description": "",
"owner_id": "5f8c0eedb5b9ef536990e6e5",
"owner": {
"id": "5f8c0eedb5b9ef536990e6e5",
"name": "Kiddy"
}
},
{
"id": "5f8c10deb5b9ef536990e6e9",
"title": "Mengerjakan tugas kuliah",
"description": "",
"owner_id": "5f8c0ef0b5b9ef536990e6e6",
"owner": {
"id": "5f8c0ef0b5b9ef536990e6e6",
"name": "Ibie"
}
}
],
"message": ""
}
Sudah hilang deh punyanya si Hudya, tinggal punya Ibie dan Kiddy~
Sekarang kita mau ngomongin soal on delete cascade yang tadi kita singgung diatas, yaitu reverse_delete_rule=CASCADE. Nah sekarang saya akan menghapus user Kiddy, dan membuktikan apakah data todosnya juga terhapus.
Sekarang kita get Todo
Taraaaa (basro)~ Sekarang hanya tinggal Todo milik Ibie. Sekarang kita get lagi users kita.
Yup tidak ada lagi user Kiddy, adanya hanya tinggal kenangan kita saja~ (uhuk).
Jadi gimana? Gampang dan keren banget kan Python FastAPI yang dipadukan dengan MongoDB?
Pada tutorial selanjutnya saya akan membuat contoh TDD dengan FastAPI Python, stay tune guys~
Bagi yang mau clone projectnya bisa banget disini.