Membuat Aplikasi Web Realtime dengan Django, RabbitMQ dan Vue.js Bagian 3: API dengan Django Rest Framework

Bagus Aji Santoso 31 Januari 2018

Membuat Aplikasi Web Realtime dengan Django, RabbitMQ dan Vue.js Bagian 3: API dengan Django Rest Framework

Artikel ini merupakan terjemahan seri tutorial Real time Chat application built with Vue, Django, RabbitMQ and uWSGI WebSockets bagian ketiga yang ditulis oleh Osaetin Daniel.

Daftar isi:

Dibagian sebelumnya kita menggunakan djoser untuk membuat backend sistem otentikasi kita dan menghubungkannya dengan aplikasi Vue.js.

Sekarang kita akan mulai membangun API menggunakan django rest framework. API ini akan memberikan endpoint yang kita butuhkan untuk memulai sesi chat baru, bergabung dengan sesi chat yang sudah ada, mengirimkan pesan lalu mengambil riwayat pesan.

Arsitektur

Sebelum memulai, mari kita diskusikan bagaimana program kita akan berjalan:

Realtime Django 3.1
  • Saat seorang user mengirim pesan, pesan ini akan dikirim ke django lewat API.
  • Setelah django menerima pesan tersebut, ia akan melanjutkannya ke RabbitMQ.
  • RabbitMQ menggunakan sebuah exchange untuk mengirim pesan broadcast ke beberapa queue. Queue ini adalah kanal komunikasi yang akan melanjutkan pesan ke klien. Workers adalah proses yang berjalan di belakang layar untuk melakukan proses broadcasting dan melanjutkan pengiriman pesan.

RabbitMQ adalah perantara dua bagian paling penting diaplikasi kita yaitu Django dan uWSGI. Ia juga membuat aplikasi kita menjadi fleksibel karena python dan django. Ada beberapa cara untuk mengirim pesan ke RabbitMQ, bahkan bisa dikirim dari command line. Ini artinya aplikasi lain tidak perlu tahu sistem aplikasi chat kita untuk berkomunikasi dengannya.

Sebagai contoh, sebuah aplikasi desktop yang ditulis dalam bahasa C# dapat mengirim pesan ke queue RabbitMQ dan pesannya tetap akan diterima oleh klien kita.

Tanpa RabbitMQ, server WebSocket uWSGI tidak tahu apa-apa tentang aplikasi django kita (bagaimana mengakses database, melakukan otentikasi, dll) karena ia berjalan di proses yang berbeda mungkin juga di server web yang berbeda tergantung konfigurasi yang dilakukan.

  • uWSGI berfungsi sebagai server websocket. Setelah klien terhubung dan kanal yang sesuai (RabbitMQ exchange) dapat menerima pesan, kita akan membaca pesan tersebut begitu diterima lalu langsung mengirimkannya ke user dengan WebSocket.

Untuk aplikasi Vue, server development webpack dapat menjadi solusi yang tepat saat karena kita baru mengembangkannya komputer lokal. Nanti jika aplikasi ini akan dideploy, kita bisa menjalankan perintah npm build yang akan menghasilkan static file untuk dijalankan oleh web server (Nginx, Apache bahkan github pages)

Implementasi

Dibagian ini, tujuan akhir kita ialah untuk mengimplementasi API dengan django rest framework. API ini akan mengijinkan user memulai sesi chat baru, bergabung dengan sesi yang sudah ada dan mengirim pesan. Kita juga bisa mengambil riwayat pesan dari sebuah sesi chat.

Mari buat app django yang baru bernama chat:

$ python manage.py startapp chat

Jangan lupa mengambahkannya ke INSTALLED_APPS.

Selanjutnya, kita akan membuat model untuk data pesan, sesi chat juga user-user yang terhubung. Mari buat model ini di models.py.

"""Models for the chat app."""

from uuid import uuid4

from django.db import models
from django.contrib.auth import get_user_model


User = get_user_model()


def deserialize_user(user):
    """Deserialize user instance to JSON."""
    return {
        'id': user.id, 'username': user.username, 'email': user.email,
        'first_name': user.first_name, 'last_name': user.last_name
    }


class TrackableDateModel(models.Model):
    """Abstract model to Track the creation/updated date for a model."""

    create_date = models.DateTimeField(auto_now_add=True)
    update_date = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True


		def _generate_unique_uri():
				"""Generates a unique uri for the chat session."""
				return str(uuid4()).replace('-', '')[:15]


class ChatSession(TrackableDateModel):
    """
		A Chat Session.

		The uri's are generated by taking the first 15 characters from a UUID
		"""

				owner = models.ForeignKey(User, on_delete=models.PROTECT)
				uri = models.URLField(default=_generate_unique_uri)


class ChatSessionMessage(TrackableDateModel):
    """Store messages for a session."""

    user = models.ForeignKey(User, on_delete=models.PROTECT)
    chat_session = models.ForeignKey(
        ChatSession, related_name='messages', on_delete=models.PROTECT
    )
    message = models.TextField(max_length=2000)

    def to_json(self):
        """deserialize message to JSON."""
        return {'user': deserialize_user(self.user), 'message': self.message}


class ChatSessionMember(TrackableDateModel):
    """Store all users in a chat session."""

    chat_session = models.ForeignKey(
        ChatSession, related_name='members', on_delete=models.PROTECT
    )
    user = models.ForeignKey(User, on_delete=models.PROTECT)

Pastikan menjalankan migrasi database sebelum melanjutkan agar tabel-tabelnya dibuat.

Langkah selanjutnya ialah dengan membuat view (endpoint API) yang memungkinkan user untuk memanipulasi data ke server.

Kita bisa mengatur django rest framework untuk membuatnya (kita tidak memakai serializer karena modelnya masih sederhana). Mari kita tulis kode di bawah pada file views.py

"""Views for the chat app."""

from django.contrib.auth import get_user_model
from .models import (
    ChatSession, ChatSessionMember, ChatSessionMessage, deserialize_user
)

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import permissions


class ChatSessionView(APIView):
    """Manage Chat sessions."""

    permission_classes = (permissions.IsAuthenticated,)

    def post(self, request, *args, **kwargs):
        """create a new chat session."""
        user = request.user

        chat_session = ChatSession.objects.create(owner=user)

        return Response({
            'status': 'SUCCESS', 'uri': chat_session.uri,
            'message': 'New chat session created'
        })

    def patch(self, request, *args, **kwargs):
        """Add a user to a chat session."""
        User = get_user_model()

        uri = kwargs['uri']
        username = request.data['username']
        user = User.objects.get(username=username)

        chat_session = ChatSession.objects.get(uri=uri)
        owner = chat_session.owner

        if owner != user:  # Only allow non owners join the room
            chat_session.members.get_or_create(
                user=user, chat_session=chat_session
            )

        owner = deserialize_user(owner)
        members = [
            deserialize_user(chat_session.user) 
            for chat_session in chat_session.members.all()
        ]
        members.insert(0, owner)  # Make the owner the first member

        return Response ({
            'status': 'SUCCESS', 'members': members,
            'message': '%s joined that chat' % user.username,
            'user': deserialize_user(user)
        })
    

class ChatSessionMessageView(APIView):
    """Create/Get Chat session messages."""

    permission_classes = (permissions.IsAuthenticated,)

    def get(self, request, *args, **kwargs):
        """return all messages in a chat session."""
        uri = kwargs['uri']

        chat_session = ChatSession.objects.get(uri=uri)
        messages = [chat_session_message.to_json() 
            for chat_session_message in chat_session.messages.all()]

        return Response({
            'id': chat_session.id, 'uri': chat_session.uri,
            'messages': messages
        })

    def post(self, request, *args, **kwargs):
        """create a new message in a chat session."""
        uri = kwargs['uri']
        message = request.data['message']

        user = request.user
        chat_session = ChatSession.objects.get(uri=uri)

        ChatSessionMessage.objects.create(
            user=user, chat_session=chat_session, message=message
        )

        return Response ({
            'status': 'SUCCESS', 'uri': chat_session.uri, 'message': message,
            'user': deserialize_user(user)
        })

Method patch untuk ChatSessionView bersifat idempotent karena memanggil rikues yang sama berkali-kali akan menghasilkan data yang sama pula. Itu artinya, user dapat bergabung ke sebuah chat room berkali-kali tapi hanya akan ada satu objek user yang sama didalam respon (termasuk di dalam tabel database).

Hal lain yang perlu diingat tentang method patch ialah ia akan mengembalikan pemilik chat room sebagai seorang member tapi di dalam database kita tidak akan menambahkan pemilik itu sebagai member room, kita hanya mengambil informasinya dan mengirimnya kembali ke aplikasi klien. Tidak perlu ada duplikasi data dengan menambahkan pemilik room sebagai membernya di database.

Kita dapat dengan mudah mengambil info user didalam method patch dengan memanggil request.user tapi pemanggilan ini hanya akan memperbanyak perintah SELECT di database.

Begini skenario sederhananya, apa yang akan terjadi jika kita memutuskan untuk mengundang teman berdasarkan username ke sebuah sesi chat. Dengan request.user kita tidak bisa melakukannya karena request.user akan merujuk pada user yang sekarang sedang melakukan rikues.

Sebaliknya, dengan username, kita cukup mengirimkan username tersebut ke serber dan server akan menggunakannya untuk mengambil user yang sesuai lalu menambahkannya ke chat room.

Lalu, jika ingin melakukan "invite multiple users", kita dapat memodifikasi kode tadi untuk mengambil sebuah list username dan menggunakannya untuk mengambil data-data dari database.

Intinya, menggunakan username membuat kode lebih fleksibel untuk pengembangan di masa mendatang.

Sekarang mari tambahkan URL untuk views yang sudah dibuat ke urls.py di app chat:

"""URL's for the chat app."""

from django.contrib import admin
from django.urls import path

from . import views

urlpatterns = [
    path('chats/', views.ChatSessionView.as_view()),
    path('chats/<uri>/', views.ChatSessionView.as_view()),
    path('chats/<uri>/messages/', views.ChatSessionMessageView.as_view()),
]

Jangan lupa untuk menambahkan URL app chat ke file urls.py utama:

from django.contrib import admin
from django.uris import path, include

uripatterns = [
    path('admin/', admin.site.uris),

    # Custom URL's
    path('auth/', include('djoser.uris')),
    path('auth/', include('djoser.uris.authtoken')),
    path('api/', include('chat.uris'))
]

Sekarang endpoint kita sudah siap dan semua user yang melakukan login dapat membuat rikues baru.

Mari kita coba di terminal:

$ curl -X POST http://127.0.0.1:8000/auth/token/create/ --data 'username=danidee&password=mypassword'
{"auth_token":"169fcd5067cc55c500f576502637281fa367b3a6"}

$ curl -X POST http://127.0.0.1:8000/api/chats/ -H 'Authorization: Token 169fcd5067cc55c500f576502637281fa367b3a6'
{"status":"SUCCESS","uri":"040213b14a02451","message":"New chat session created"}

$ curl -X POST http://127.0.0.1:8000/auth/users/create/ --data 'username=daniel&password=mypassword'
{"email":"","username":"daniel","id":2}

$ curl -X POST http://127.0.0.1:8000/auth/token/create/ --data 'username=daniel&password=mypassword'
{"auth_token":"9c3ea2d194d7236ac68d2faefba017c8426a8484"}

$ curl -X PATCH http://127.0.0.1:8000/api/chats/040213b14a02451/ --data 'username=daniel' -H 'Authorization: Token 9c3ea2d194d7236ac68d2faefba017c8426a8484'
{"status":"SUCCESS","members":[{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""},{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}],"message":"daniel joined that chat","user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}}

Selanjutnya kita coba mengirim pesan:

$ curl -X POST http://127.0.0.1:8000/api/chats/040213b14a02451/messages/ --data 'message=Hello!' -H 'Authorization: Token 169fcd5067cc55c500f576502637281fa367b3a6'
{"status":"SUCCESS","uri":"040213b14a02451","message":"Hello!","user":{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""}}

$ curl -X POST http://127.0.0.1:8000/api/chats/040213b14a02451/messages/ --data 'message=Hey whatsup!' -H 'Authorization: Token 9c3ea2d194d7236ac68d2faefba017c8426a8484'
{"status":"SUCCESS","uri":"040213b14a02451","message":"Hey whatsup! i dey","user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""}}

Terakhir kita coba membaca riwayat pesan:

$ curl http://127.0.0.1:8000/api/chats/040213b14a02451/messages/ -H 'Authorization: Token 169fcd5067cc55c500f576502637281fa367b3a6'
{"id":1,"uri":"040213b14a02451","messages":[{"user":{"id":1,"username":"danidee","email":"osaetindaniel@gmail.com","first_name":"","last_name":""},"message":"Hello!"},{"user":{"id":2,"username":"daniel","email":"","first_name":"","last_name":""},"message":"Hey whatsup!"}]}

Selamat! Kita sudah berhasil membangun API yang memungkinkan user untuk berkomunikasi satu sama lain lewat sesi chat dan mengundah user lain untuk bergabung dengan sesi chat.

Selanjutnya kita akan membangun UI aplikasi chat (Vue) dan memanggil method-method di atas didalamnya.

Kode akhirnya bisa dilihat di Github.