Blog AWS Indonesia

Membangun Aplikasi Chat Full-stack dengan AWS dan NextJS

Aplikasi chat modern membutuhkan serangkaian fitur yang kaya. Fitur-fitur ini termasuk penyimpanan file, real-time update, dan kemampuan untuk mengambil data pada klien dan server.

Pada umumnya, hal ini berarti menyatukan banyak layanan pihak ketiga, atau menghabiskan waktu development untuk membuat solusi khusus. Terlebih lagi, hal ini memperlambat time-to-market dan menimbulkan beberapa titik kegagalan.

Untuk mendemonstrasikan fitur-fitur ini dan bagaimana AWS memecahkan kendala yang umum terjadi, kami telah memperbarui aplikasi chat real-time kami. Versi ini telah didesain ulang untuk menekankan betapa sederhananya tim-tim dapat mengontrol penuh pengelolaan aplikasi secara lokal maupun di AWS.

Versi aplikasi ini terdiri dari teknologi berikut:

Frontend

Backend

Frontend chat UI

Gambar 1. Tampilan Chat di Frontend

Gambaran umum arsitektur backend

Diagram Arsitektur Backend

Gambar 2. Diagram Arsitektur Backend

Gambaran Umum

Berikut adalah gambaran umum tentang bagaimana kita akan menggunakan layanan backend kita:

  • Amazon Cognito: Proyek ini menggunakan kumpulan pengguna (User Pools), kumpulan identitas (Identity Pools), dan grup kumpulan pengguna (User Pools Group).
  • AWS Lambda: Saat pengguna mendaftar ke aplikasi kita, kita menggunakan trigger postConfirmation di Amazon Cognito untuk menambahkannya ke database. Ini memungkinkan pengguna untuk mencari anggota chat lainnya.
  • AWS AppSync GraphQL dan Pub/Sub API: Memanfaatkan hubungannya dengan Cognito, kita mengembangkan API dengan AMAZON_COGNITO_USER_POOLS ditetapkan sebagai authMode. Dengan AWS AppSync Resolvers, kita membuat pemetaan langsung dari API ke Database kita. Selain membuat API endpoint, AWS AppSync juga membuat serverless WebSockets endpoint untuk mengaktifkan notifikasi real-time. Semua ini fully-managed, yang berarti bahwa kita tidak perlu menyiapkan server atau mengelola connection pools.
  • Amazon S3: Kita akan mengizinkan pengguna meng-upload gambar yang hanya dapat diakses oleh pengguna yang telah sign-in.
  • Amazon DynamoDB: Seperti yang terlihat dari tangkapan layar di atas, aplikasi ini menggunakan arsitektur desain multi-table, bukan desain single-table. Tabel User akan berisi informasi tentang pengguna kita yang terautentikasi. Tabel Message berisi detail, tidak hanya tentang pesan teks, tetapi juga ID gambar untuk Amazon S3. Tabel Room digunakan untuk mencakup pesan dan langganan pesan real-time. Selain itu, aplikasi ini menyiapkan Global Secondary Index (GSI) untuk memungkinkan pola akses yang berbeda.

Dari perspektif Infrastructure-as-Code (IaC), aplikasi ini memanfaatkan AWS CDK untuk membuat layanan yang disebutkan di atas. CDK mendukung banyak bahasa pemrograman yang berbeda. Namun, tim yang memiliki komponen frontend kemungkinan akan mendapat manfaat dari TypeScript, karena dapat digunakan di frontend serta backend.

Melalui penggunaan construct, developer dapat menyusun layanan mereka menjadi kode yang dapat digunakan kembali. Untuk melihat bagaimana ini bekerja, berikut adalah cuplikan dari backend:

const databaseStack = new DatabaseStack(app, 'DatabaseStack', {})

const authStack = new AuthStack(app, 'AuthStack', {
    stage: 'dev',
    hasCognitoGroups: true,
    groupNames: ['admin'],
    userpoolConstructName: 'ChatUserPool',
    identitypoolConstructName: 'ChatIdentityPool',
    userTable: databaseStack.userTable,
})

const fileStorageStack = new FileStorageStack(app, 'FileStorageStack', {
    authenticatedRole: authStack.authenticatedRole,
    unauthenticatedRole: authStack.unauthenticatedRole,
    allowedOrigins: ['http://localhost:3000'],
})

const apiStack = new APIStack(app, 'AppSyncAPIStack', {
    userpool: authStack.userpool,
    roomTable: databaseStack.roomTable,
    messageTable: databaseStack.messageTable,
    userTable: databaseStack.userTable,
    unauthenticatedRole: authStack.unauthenticatedRole,
})

Memahami API yang dibuat

AWS AppSync memungkinkan pembuatan GraphQL serverless API yang menyederhanakan pengembangan aplikasi dengan menyediakan satu endpoint untuk meng-query atau memperbarui data dengan aman dari beberapa sumber data. Lebih jauh lagi, GraphQL subscription memungkinkan pengalaman aplikasi real-time yang interaktif setiap kali data diperbarui.

Di backend, AWS AppSync terhubung ke sumber datanya melalui penggunaan mapping template. Request mapping template membuat dokumen JSON yang memberi tahu bagaimana data dikirim ke sumber datanya (dalam hal ini, DynamoDB). Response mapping template melakukan yang sebaliknya: dokumennya memberi tahu bagaimana data dari sumber data dikirim kembali ke API.

Kita dapat melihat ini dilakukan dengan meninjau Auth Stack:

messageTableDataSource.createResolver({
    typeName: 'Mutation',
    fieldName: 'updateMessage',
    requestMappingTemplate: MappingTemplate.fromFile(
        path.join(
            __dirname,
            'graphql/mappingTemplates/Mutation.updateMessage.req.vtl'
        )
    ),
    responseMappingTemplate: MappingTemplate.dynamoDbResultItem(),
})

requestMappingTemplate mentransformasi request kita ke DynamoDB dengan menggunakan file Mutation.updateMessage.req.vtl :

{
  "version" : "2018-05-29",
  "operation" : "UpdateItem",
  "key": {"id": $util.dynamodb.toDynamoDBJson($ctx.args.input.id)},
  "update" : {
      "expression" : "SET #updatedAt = :updatedAt, #content = :content",
      "expressionNames" : {
          "#updatedAt" : "updatedAt",
          "#content": "content"
      },
      "expressionValues" : {
          ":updatedAt" : $util.dynamodb.toDynamoDBJson($util.time.nowISO8601()),
          ":content": $util.dynamodb.toDynamoDBJson($ctx.args.input.content)
      }
  }
}

Di atas, kita mengambil id argumen yang ditemukan pada objek arguments masuk (disingkat args) dan mengaturnya sebagai key. Ini memberi tahu DynamoDB item mana yang ingin kita perbarui.

Melalui penggunaan expression, kita memberi tahu DynamoDB untuk memperbarui: waktu pesan diperbarui dan konten terbarunya.

Karena pengiriman data kembali ke klien sering kali terkait pengiriman satu atau banyak item, konstruksi AWS AppSync CDK dilengkapi dengan beberapa metode bawaan untuk mendapatkan respon tersebut:

responseMappingTemplate: MappingTemplate.dynamoDbResultItem()

Memanfaatkan Pustaka Amplify untuk menghubungkan frontend Anda ke backend Anda

Setelah mendaftar, pengguna dapat membuat chat room untuk diikuti pengguna lain. Begitu berada di ruang chat, mereka dapat mengirim pesan teks atau gambar. AWS AppSync secara otomatis mengotorisasi pengguna yang valid dan meneruskan permintaan yang valid ke DynamoDB untuk menyimpan pesan dan data ruangan.

Untuk mengaktifkan fungsi ini, frontend harus dikonfigurasi untuk memanfaatkan nilai yang diekspor dari backend kita. Untuk instruksi lengkap tentang cara mengatur frontend kita, lihat panduan memulai.

Dengan memanfaatkan pustaka frontend Amplify, panggilan API dan pengembangan antarmuka pengguna (UI) menjadi lebih mudah. Misalnya, berikut ini adalah gambaran umum tentang bagaimana kita akan menggunakan berbagai metode:

  • Storage.get(ITEM_KEY): Mengambil item dari S3 bucket kita. Metode ini mengembalikan signed-URL.
  • Auth.currentAuthenticatedUser(): Mengambil data pengguna dari JWTdan mengembalikan objek yang berisi detail masuk dan atribut dari Cognito.
  • API.graphql({query, variables, authMode}): Buat permintaan yang diautentikasi ke GraphQL API kita. Metode ini berfungsi untuk query, mutasi, dan dapat diperluas untuk memungkinkan subscription.

Mengelola pendaftaran pengguna

API kita dibuat dengan Amazon Cognito sebagai strategi otorisasi bawaan.

Untuk mengautentikasi pengguna kita di frontend, kita menggunakan komponen withAuthenticator dari paket @aws-amplify/ui-react.

Dengan menambahkan konfigurasi berikut, kita dapat menyesuaikan UI agar sesuai dengan kasus penggunaan kita:

export default withAuthenticator(Home, {
    signUpAttributes: ['email', 'given_name', 'family_name'],
})
amplify membuat layar akun

Gambar 3. Tampilan pembuatan akun di Amplify

Mengirim dan menerima pesan menggunakan GraphQL

Setelah pengguna kita diautentikasi dalam aplikasi kita, mereka dapat mulai melakukan panggilan ke API kita. Misalnya, di beranda pengguna mungkin ingin disajikan dengan daftar room yang tersedia di mana mereka dapat mulai chat:

API.graphql({
    query: listRooms,
}).then(({ data }) => {
    setRooms(data.listRooms.items)
})

Membuat panggilan terautentikasi di sisi klien berfungsi dengan baik. Namun, ada kalanya memanggil sisi server API kita lebih mudah. Untuk mendemonstrasikan cara kerjanya, kita memanfaatkan fungsi withSSRContext untuk mengakses detail yang disimpan dari sesi cookie kita:

export async function getServerSideProps(context) {
    // pass the context to an Amplify function
    const { API, Auth } = withSSRContext(context)
    try {
        const user = await Auth.currentAuthenticatedUser()
        const { data } = await API.graphql({
            query: listRooms,
        })
        const currentRoomData = data.listRooms.items.find(
            (room) => room.id === context.params.roomId
        )
        return {
            props: {
                currentRoomData,
                username: user.username,
                roomsList: data.listRooms.items,
            },
        }
        // if there's an error, perform a server-side redirect
    } catch (err) {
        return {
            redirect: {
                destination: '/',
                permanent: false,
            },
        }
    }
}

Di atas, kita memeriksa apakah ada pengguna, dan kemudian tampilkan daftar chat room. Jika ada kesalahan dalam melakukannya, maka kita arahkan pengguna kembali ke frontend.

Mengelola Upload Gambar

Bagian visual utama dari aplikasi kita adalah memungkinkan pengguna kita untuk meng-upload gambar. Di backend, kita membuat kebijakan terkelola untuk S3 bucket yang mengikuti praktik terbaik Amplify. Kebijakan ini memblokir akses anonim ke file kita dan memungkinkan pengguna yang masuk untuk mengambil dan meng-upload file ke direktori publik.

Dua aplikasi chat terpisah berdampingan, mengirim pesan

Gambar 4. Dua aplikasi chat terpisah berdampingan, mengirim pesan

Untuk mencapai ini, skema kita mendefinisikan Message sebagai sebuah type di mana MessageContentnya dapat berisi imageId atau text.

type Message {
id: ID!
content: MessageContent!
owner: String!
createdAt: AWSDateTime!
updatedAt: AWSDateTime!
roomId: ID!
}
type MessageContent {
text: String
imageId: String
}

Kita menentukan key untuk gambar karena modul Storage dari Amplify akan secara otomatis menempatkan gambar kita di Amazon S3 selama kita memberinya nama untuk file tersebut, serta data file itu sendiri.

const uploadFile = async (selectedPic) => {
    const { key } = await Storage.put(selectedPic.name, selectedPic, {
        contentType: selectedPic.type,
    })

    return key
}

Kesimpulan

Dalam artikel ini, kami menunjukkan bagaimana tim dapat membangun aplikasi chat yang dapat scaling dan berisi serangkaian fitur kaya yang diharapkan oleh pelanggan. Dengan memanfaatkan AWS CDK, tim memiliki fleksibilitas pada frontend dan backend yang mengerjakan stack masing-masing, sambil tetap memungkinkan developer full-stack untuk mengelola kedua stacks dalam bahasa yang sama.

Kita juga melihat bagaimana AWS AppSync mengekspos satu endpoint ke tim frontend tanpa perlu khawatir tentang server atau membuat abstraksi backend yang kompleks. Tim frontend dapat dengan mudah menggunakan strongly-typed data model yang disediakan oleh skema GraphQL, sehingga memungkinkan mereka untuk fokus pada pengalaman pengguna.

Di frontend, tim dapat fokus menggunakan layanan backend yang dibuat dengan Amplify UI primitive dan JavaScript binding.

Seperti yang akan kita lihat di posting mendatang, aplikasi ini dapat diperluas untuk memungkinkan notifikasi dan penanganan subscription yang lebih baik.

Untuk membaca selengkapnya tentang bagaimana Anda dapat memanfaatkan AWS AppSync dalam proyek Anda berikutnya, kunjungi gambaran umum layanan dan lihat dokumentasi pustaka Amplify untuk penggunaan dan penyiapan frontend.

Artikel ini diterjemahkan dari artikel asli berjudul “Building a full-stack chat application with AWS and NextJS” yang ditulis oleh Michael Liendo.

Arief Hidayat

Arief Hidayat

Arief Hidayat is a Senior Solutions Architect at Amazon Web Services Indonesia. As technical advisor, he helps enterprise companies in Indonesia to have smooth journey on the cloud.