Trivia Quiz App using the NATS Key-Value Store and the Gofr framework

Vaidehi Adhi - Feb 23 - - Dev Community

What is NATS?

NATS is a lightweight, high-performance messaging system designed for cloud-native applications. Its Key-Value Store (KV) feature allows you to store and retrieve data efficiently, making it ideal for applications that require fast access to stateful data. NATS provides features like persistence, history, and multi-tenancy, which are beneficial for building scalable applications.

Project Overview

The Trivia Quiz Application allows users to:

  • Create and manage trivia questions
  • Take quizzes and answer trivia questions
  • View scores and leaderboards

This application provides an engaging platform for users to challenge themselves and others in a fun trivia format.

Setting Up NATS with Docker

To get started with NATS, you can easily run it using Docker:

  1. Install Docker if you haven't already.
  2. Pull and run the NATS container:
docker run -p 4222:4222 -p 8222:8222 nats:latest
Enter fullscreen mode Exit fullscreen mode

This command starts a NATS server that listens on port 4222 for client connections and exposes a monitoring interface on port 8222.

Database Schema

Our application will use a simple key-value structure to store trivia questions and user scores. Each question will be stored under a unique key (its ID), with its details serialized as JSON.

Example Key-Value Structure

  • Key: question:
  • Value: JSON object containing question, options, correct_answer
  • Key: score:
  • Value: JSON object containing username and score

Integrating NATS with Gofr

The integration between NATS and Gofr is straightforward. We will set up the NATS client in our application:

package main

import (
    "encoding/json"
    "fmt"
    "gofr.dev/pkg/gofr"
    "gofr.dev/pkg/gofr/datasource/kv-store/nats"
    "gofr.dev/pkg/gofr/http"
)

type Question struct {
    ID            string   `json:"id,omitempty"`
    Text          string   `json:"question"`
    Options       []string `json:"options"`
    CorrectAnswer string   `json:"correct_answer"`
}

type Score struct {
    Username string `json:"username"`
    Score    int    `json:"score"`
}

func main() {
    app := gofr.New()

    app.AddKVStore(nats.New(nats.Configs{
        Server: "nats://localhost:4222",
        Bucket: "trivia",
    }))

    app.POST("/question", CreateQuestion)
    app.POST("/answer", SubmitAnswer)
    app.GET("/leaderboard", GetLeaderboard)

    app.Run()
}
Enter fullscreen mode Exit fullscreen mode

Core Data Models

We define our data structures for trivia questions and scores as follows:

type Question struct {
    ID            string   `json:"id,omitempty"`
    Text          string   `json:"question"`
    Options       []string `json:"options"`
    CorrectAnswer string   `json:"correct_answer"`
}

type Score struct {
    Username string `json:"username"`
    Score    int    `json:"score"`
}
Enter fullscreen mode Exit fullscreen mode

API Endpoints

  1. Create Question (POST /question) This endpoint allows administrators to add new trivia questions:
POST http://localhost:9000/question
Content-Type: application/json

{
    "question": "What is the capital of France?",
    "options": ["Berlin", "Madrid", "Paris", "Rome"],
    "correct_answer": "Paris"
}
Enter fullscreen mode Exit fullscreen mode
  1. Submit Answer (POST /answer)
POST http://localhost:9000/answer
Content-Type: application/json

{
    "username": "john_doe",
    "question_id": "question:12345",
    "selected_option": "Paris"
}
Enter fullscreen mode Exit fullscreen mode

3. View Leaderboard (GET /leaderboard)

GET http://localhost:9000/leaderboard
Enter fullscreen mode Exit fullscreen mode

Implementation Details

The core of our implementation lies in defining the handlers for each endpoint:

func CreateQuestion(ctx *gofr.Context) (interface{}, error) {
    var question Question
    if err := ctx.Bind(&question); err != nil {
        return nil, http.ErrorInvalidParam{Params: []string{"body"}}
    }

    question.ID = uuid.New().String()
    questionData, err := json.Marshal(question)
    if err != nil {
        return nil, fmt.Errorf("failed to serialize question")
    }

    if err := ctx.KVStore.Set(ctx, fmt.Sprintf("question:%s", question.ID), string(questionData)); err != nil {
        return nil, err
    }

    return question, nil
}

func SubmitAnswer(ctx *gofr.Context) (interface{}, error) {
    var request struct {
        Username       string `json:"username"`
        QuestionID     string `json:"question_id"`
        SelectedOption string `json:"selected_option"`
    }
    if err := ctx.Bind(&request); err != nil {
        return nil, http.ErrorInvalidParam{Params: []string{"body"}}
    }

    value, err := ctx.KVStore.Get(ctx, request.QuestionID)
    if err != nil {
        return nil, fmt.Errorf("question not found")
    }

    var question Question
    if err := json.Unmarshal([]byte(value), &question); err != nil {
        return nil, fmt.Errorf("failed to parse question data")
    }

    score := 0
    if request.SelectedOption == question.CorrectAnswer {
        score = 1 // Increment score for correct answer
    }

    currentScoreKey := fmt.Sprintf("score:%s", request.Username)
    scoreData := Score{Username: request.Username, Score: score}

    scoreJSON, _ := json.Marshal(scoreData)
    if err := ctx.KVStore.Set(ctx, currentScoreKey, string(scoreJSON)); err != nil {
        return nil, err
    }

    return map[string]string{"message": "Answer submitted!"}, nil
}

func GetLeaderboard(ctx *gofr.Context) (interface{}, error) {
    scores := []Score{}
    for _, key := range ctx.KVStore.GetKeys("score:*") { // Hypothetical function to get all keys matching pattern
        value, err := ctx.KVStore.Get(ctx, key)
        if err == nil {
            var score Score
            json.Unmarshal([]byte(value), &score)
            scores = append(scores, score)
        }
    }

    sort.Slice(scores, func(i, j int) bool { return scores[i].Score > scores[j].Score })

    return scores[:10], nil // Return top 10 scores
}
Enter fullscreen mode Exit fullscreen mode

Testing the System

You can use Postman or curl to test each endpoint. For example, to create a new question:

curl -X POST http://localhost:9000/question \
     -H "Content-Type: application/json" \
     -d '{"question":"What is the capital of France?","options":["Berlin","Madrid","Paris","Rome"],"correct_answer":"Paris"}'
Enter fullscreen mode Exit fullscreen mode

Conclusion

This project demonstrates the power of using NATS Key-Value Store with the Gofr framework. The combination provides an efficient way to build scalable applications that require fast data retrieval and storage. The Trivia Quiz Application offers a fun way for users to engage with trivia questions while keeping track of their scores.

Feel free to explore this project further!

If you found this article helpful or interesting, consider starring the Gofr repository on GitHub!

To know more about GoFr check out their official documentationand
Github Repo

. . . . .