Module Beginner 18 Oktober 2020

Tutorial RESTful API dengan FastAPI Python + MongoDB Part 1 — Instalasi dan CRUD

Belajar membuat RESTful API dengan salah satu framework Python tercepat saat ini!

Tutorial RESTful API dengan FastAPI Python + MongoDB Part 1 — Instalasi dan CRUD

Belajar membuat RESTful API dengan salah satu framework Python tercepat saat ini!

Image 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.

Image 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.

Image Sekarang kita akan mulai masuk ke init.pynya dulu.

from fastapi import FastAPIfrom mongoengine import connectfrom starlette.testclient import TestClientfrom app.routers import users, todosapp = 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 Usersclass 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 Requestfrom starlette.responses import JSONResponsefrom app import responsefrom app.models.user import Usersfrom app.transformers import UserTransformerclass 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 Requestfrom starlette.responses import JSONResponsefrom app import responsefrom app.models.todo import Todosfrom app.transformers import TodoTransformerclass 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 arraydef singleTransform(values): return { “id”: str(values.id), “name”: values.name, }transformers/TodoTransformer.py

from app.transformers import UserTransformerdef transform(items): array = [] for item in items: array.append(singleTransform(item)) return arraydef 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.

Image 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 APIRouterfrom starlette.requests import Requestfrom app.controllers.UserController import UserController as controllerrouter = 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 APIRouterfrom starlette.requests import Requestfrom app.controllers.TodoController import TodoController as controllerrouter = 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 statusfrom starlette.responses import JSONResponsedef 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 uvicornfrom app import appif 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=debugdia bakalan mirip sama flask yang otomatis reload kalo ada code yang berubah!

Oke sekarang kita gasken cek ombak dulu.

Image

Image

Image

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.

Image 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.

Image Dan sekarang todo list si Hudya saya hapus.

Image 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.

Image Sekarang kita get Todo

Image Taraaaa (basro)~ Sekarang hanya tinggal Todo milik Ibie. Sekarang kita get lagi users kita.

Image 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.

kiddyxyz/restful-fastapi-mongo*You can’t perform that action at this time. You signed in with another tab or window. You signed out in another tab or…*github.com


Artikel ini merupakan konten legacy dari blog Medium (Tahun 2020). Beberapa konsep atau sintaks mungkin sudah mengalami perubahan pada versi terbaru.