SLC-S23W6 - Backend & golang- Candidate Evaluation Platform

in #techclub3 days ago

image.png

Welcome to the final week of our Tech & Dev Club learning series! Over the past five weeks, we've covered project planning, containerization with Docker, frontend development with React, and database management. Now, it's time to tie everything together with backend development using Golang.

What You'll Learn This Week

  • Golang as a backend language
  • RESTful API design principles
  • API documentation with Swagger
  • Authentication and middleware
  • Controller-Service-Repository pattern
  • Real examples from the Candidly project

Why Golang for Backend Development?


Golang Mascot

Golang offers several benefits for backend development:

  1. Performance: Compiled to machine code, making it very fast
  2. Concurrency: Goroutines make concurrent operations simple
  3. Simplicity: Easy to learn with clean syntax
  4. Strong Standard Library: Includes HTTP server components
  5. Growing Ecosystem: Many third-party packages available

Project Architecture

Our backend follows a layered architecture:


image.png
Backend Architecture Diagram

  1. Router Layer: Handles HTTP requests
  2. Controller Layer: Processes input and returns responses
  3. Service Layer: Contains business logic
  4. Repository Layer: Interacts with the database
  5. Model Layer: Defines data structures

Setting Up the Server

Here's how we initialize our server in Golang:

// server/server.go
package server

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/cors"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "log"
)

func Init() {
    app := fiber.New()
    
    // Middleware
    app.Use(logger.New())
    app.Use(cors.New())
    
    // Setup routes
    SetupRoutes(app)
    
    // Start server
    log.Fatal(app.Listen(":8080"))
}

API Routing with Fiber

image.png
API Request Flow Diagram

We use the Fiber framework to define our API routes:

// server/router.go
package server

import (
    "github.com/dev/test/controllers"
    "github.com/dev/test/middleware"
    "github.com/gofiber/fiber/v2"
)

func SetupRoutes(app *fiber.App) {
    // API group
    api := app.Group("/api")
    
    // Public routes
    api.Post("/login", controllers.Login)
    
    // Auth middleware for protected routes
    auth := api.Group("/")
    auth.Use(middleware.JWTAuth())
    
    // Questions routes
    auth.Get("/questions", controllers.GetQuestions)
    auth.Post("/questions/edit", controllers.CreateQuestion)
    auth.Get("/questions/edit/:id", controllers.GetQuestion)
    auth.Post("/questions/edit/:id", controllers.UpdateQuestion)
    
    // Tests routes
    auth.Post("/my-tests", controllers.CreateTest)
    auth.Get("/my-tests/:id", controllers.GetTest)
    auth.Post("/my-tests/:test_id", controllers.UpdateTest)
    
    // And more routes...
}

JWT Authentication

image.png
Authentication Flow Diagram

Security is crucial for our application. We implement JWT-based authentication:

// middleware/jwt_auth.go
package middleware

import (
    "github.com/gofiber/fiber/v2"
    "github.com/golang-jwt/jwt/v4"
    "os"
    "strings"
)

func JWTAuth() fiber.Handler {
    return func(c *fiber.Ctx) error {
        // Get authorization header
        authHeader := c.Get("Authorization")
        
        // Check if auth header exists
        if authHeader == "" {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error": "Unauthorized",
            })
        }
        
        // Extract the token
        tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
        
        // Parse and validate the token
        token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            return []byte(os.Getenv("JWT_SECRET")), nil
        })
        
        if err != nil || !token.Valid {
            return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
                "error": "Invalid or expired token",
            })
        }
        
        // Set the user ID in context for controllers to access
        claims := token.Claims.(jwt.MapClaims)
        c.Locals("userID", claims["id"])
        
        return c.Next()
    }
}

Controller Layer

Controllers handle HTTP requests and responses:

// controllers/question.go
package controllers

import (
    "github.com/dev/test/models"
    "github.com/dev/test/services"
    "github.com/gofiber/fiber/v2"
    "strconv"
)

// GetQuestions returns all questions
func GetQuestions(c *fiber.Ctx) error {
    // Get query parameters
    questionType := c.Query("type")
    difficulty := c.Query("difficulty")
    
    // Get user ID from context
    userID := c.Locals("userID").(float64)
    
    // Get questions using service
    questions, err := services.GetQuestionsByFilter(uint(userID), questionType, difficulty)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Could not retrieve questions",
        })
    }
    
    return c.JSON(questions)
}

// CreateQuestion creates a new question
func CreateQuestion(c *fiber.Ctx) error {
    var input models.CreateQuestionInput
    
    // Parse request body
    if err := c.BodyParser(&input); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid input",
        })
    }
    
    // Validate input
    if err := validate.Struct(input); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Validation failed",
        })
    }
    
    // Get user ID from context
    userID := c.Locals("userID").(float64)
    
    // Create question using service
    question, err := services.CreateQuestion(input, uint(userID))
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Could not create question",
        })
    }
    
    return c.JSON(question)
}

// GetQuestion returns a specific question
func GetQuestion(c *fiber.Ctx) error {
    id, err := strconv.ParseUint(c.Params("id"), 10, 32)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid ID format",
        })
    }
    
    question, err := services.GetQuestionByID(uint(id))
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "Question not found",
        })
    }
    
    return c.JSON(question)
}

// UpdateQuestion updates a specific question
func UpdateQuestion(c *fiber.Ctx) error {
    id, err := strconv.ParseUint(c.Params("id"), 10, 32)
    if err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid ID format",
        })
    }
    
    var input models.CreateQuestionInput
    if err := c.BodyParser(&input); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Invalid input",
        })
    }
    
    // Validate input
    if err := validate.Struct(input); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "Validation failed",
        })
    }
    
    question, err := services.UpdateQuestion(uint(id), input)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
            "error": "Could not update question",
        })
    }
    
    return c.JSON(question)
}

Service Layer

Services contain business logic:

// services/question.go
package services

import (
    "github.com/dev/test/models"
    "github.com/dev/test/repositories"
)

// GetQuestionsByFilter retrieves questions by type and difficulty
func GetQuestionsByFilter(userID uint, questionType, difficulty string) ([]models.Question, error) {
    return repositories.GetQuestionsByFilter(userID, questionType, difficulty)
}

// CreateQuestion creates a new question
func CreateQuestion(input models.CreateQuestionInput, userID uint) (models.Question, error) {
    // Create the question entity
    question := models.Question{
        Name:         input.Name,
        QuestionText: input.QuestionText,
        Difficulty:   input.Difficulty,
        Type:         input.Type,
        ExpectedTime: input.ExpectedTime,
        MaxPoints:    input.MaxPoints,
        FileReadMe:   input.FileReadMe,
        UserID:       userID,
    }
    
    // Handle skill association
    if input.SkillID > 0 {
        skill, err := repositories.GetSkillByID(input.SkillID)
        if err == nil {
            question.SkillID = skill.ID
        }
    } else if input.SkillName != "" {
        // Try to find existing skill or create new one
        skill, err := repositories.GetSkillByName(input.SkillName)
        if err != nil {
            // Create new skill
            newSkill, err := repositories.CreateSkill(models.Skill{Name: input.SkillName})
            if err == nil {
                question.SkillID = newSkill.ID
            }
        } else {
            question.SkillID = skill.ID
        }
    }
    
    // Save the question
    savedQuestion, err := repositories.CreateQuestion(question)
    if err != nil {
        return models.Question{}, err
    }
    
    // Save choices if present
    if len(input.Choices) > 0 {
        for _, c := range input.Choices {
            choice := models.Choices{
                ChoiceText: c.ChoiceText,
                IsAnswer:   c.IsAnswer,
                QuestionID: savedQuestion.ID,
            }
            _, err := repositories.CreateChoice(choice)
            if err != nil {
                return savedQuestion, err
            }
        }
    }
    
    // Return the saved question with all relations loaded
    return repositories.GetQuestionByID(savedQuestion.ID)
}

// Additional service methods...

Repository Layer

Repositories handle database operations:

// repositories/question.go
package repositories

import (
    "github.com/dev/test/database"
    "github.com/dev/test/models"
)

// GetQuestionsByFilter retrieves questions by type and difficulty
func GetQuestionsByFilter(userID uint, questionType, difficulty string) ([]models.Question, error) {
    var questions []models.Question
    query := database.DB
    
    // Apply filters if provided
    if questionType != "" {
        query = query.Where("type = ?", questionType)
    }
    
    if difficulty != "" {
        query = query.Where("difficulty = ?", difficulty)
    }
    
    // Only show questions created by this user
    query = query.Where("user_id = ?", userID)
    
    // Execute query with preloaded relations
    result := query.Preload("Choices").Preload("Skill").Find(&questions)
    
    return questions, result.Error
}

// GetQuestionByID retrieves a question by ID
func GetQuestionByID(id uint) (models.Question, error) {
    var question models.Question
    result := database.DB.
        Preload("Choices").
        Preload("Skill").
        First(&question, id)
        
    return question, result.Error
}

// CreateQuestion creates a new question
func CreateQuestion(question models.Question) (models.Question, error) {
    result := database.DB.Create(&question)
    return question, result.Error
}

// Additional repository methods...

API Documentation with Swagger

One of the most important aspects of API development is documentation. We use Swagger to document our API endpoints:


image.png
How Swagger Work

Setting Up Swagger

We use swaggo/swag to generate Swagger documentation from code annotations:

// main.go
package main

import (
    "github.com/dev/test/docs"
    "github.com/dev/test/server"
    "os"
)

// @title test
// @version 1.0
// @description this is an application web of interview assessment tests for interviewing out of the box.
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @BasePath /api

func main() {
    // Set JWT secret key
    os.Setenv("key", "123456781234567812345678")
    
    // Initialize swagger info
    docs.SwaggerInfo.Title = "Candidly API"
    docs.SwaggerInfo.Description = "API for interview assessment tests"
    docs.SwaggerInfo.Version = "1.0"
    docs.SwaggerInfo.BasePath = "/api"
    
    // Start server
    server.Init()
}

Annotating Controllers for Swagger

We use special comments to annotate our controllers for Swagger documentation:

// controllers/question.go

// GetQuestions godoc
// @Summary find a question
// @Description find a question by type or difficulty
// @Tags question
// @Accept json
// @Produce json
// @Param type query string false "question search by type"
// @Param difficulty query string false "question search by difficulty"
// @Success 200 {array} models.Question
// @Security Authorization
// @Router /questions [get]
func GetQuestions(c *fiber.Ctx) error {
    // Implementation...
}

// CreateQuestion godoc
// @Summary add new question
// @Description create new question by json
// @Tags question
// @Accept json
// @Produce json
// @Param question body models.CreateQuestionInput true "Add question"
// @Success 200 {object} models.Question
// @Security Authorization
// @Router /questions/edit [post]
func CreateQuestion(c *fiber.Ctx) error {
    // Implementation...
}

Generated Swagger Documentation

The swagger annotations generate comprehensive API documentation:

{
  "/questions": {
    "get": {
      "security": [
        {
          "Authorization": []
        }
      ],
      "description": "find a question by type or difficulty",
      "consumes": [
        "application/json"
      ],
      "produces": [
        "application/json"
      ],
      "tags": [
        "question"
      ],
      "summary": "find a question",
      "parameters": [
        {
          "type": "string",
          "description": "question search by type",
          "name": "type",
          "in": "query"
        },
        {
          "type": "string",
          "description": "question search by difficulty",
          "name": "difficulty",
          "in": "query"
        }
      ],
      "responses": {
        "200": {
          "description": "OK",
          "schema": {
            "type": "array",
            "items": {
              "$ref": "#/definitions/models.Question"
            }
          }
        }
      }
    }
  }
}

Integration Testing

image.png
Test Evaluation Process Diagram

Testing is crucial for ensuring reliable API endpoints. We use Go's testing package:

// controllers/question_test.go
package controllers

import (
    "encoding/json"
    "github.com/dev/test/models"
    "github.com/gofiber/fiber/v2"
    "github.com/stretchr/testify/assert"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
)

func TestGetQuestions(t *testing.T) {
    // Setup
    app := fiber.New()
    app.Get("/api/questions", func(c *fiber.Ctx) error {
        c.Locals("userID", float64(1)) // Mock authenticated user
        return GetQuestions(c)
    })
    
    // Execute request
    req := httptest.NewRequest("GET", "/api/questions?type=MCQ&difficulty=easy", nil)
    resp, _ := app.Test(req)
    
    // Assert status code
    assert.Equal(t, http.StatusOK, resp.StatusCode)
    
    // Parse response body
    body, _ := ioutil.ReadAll(resp.Body)
    var questions []models.Question
    json.Unmarshal(body, &questions)
    
    // Assert response
    assert.NotEmpty(t, questions)
    assert.Equal(t, "MCQ", questions[0].Type)
    assert.Equal(t, "easy", questions[0].Difficulty)
}

func TestCreateQuestion(t *testing.T) {
    // Setup
    app := fiber.New()
    app.Post("/api/questions/edit", func(c *fiber.Ctx) error {
        c.Locals("userID", float64(1)) // Mock authenticated user
        return CreateQuestion(c)
    })
    
    // Prepare request
    questionJSON := `{
        "name": "Test Question",
        "question_text": "What is the capital of France?",
        "type": "MCQ",
        "difficulty": "easy",
        "expected_time": 60,
        "max_points": 10,
        "choices": [
            {
                "choice_text": "Paris",
                "is_answer": true
            },
            {
                "choice_text": "London",
                "is_answer": false
            }
        ]
    }`
    
    // Execute request
    req := httptest.NewRequest("POST", "/api/questions/edit", strings.NewReader(questionJSON))
    req.Header.Set("Content-Type", "application/json")
    resp, _ := app.Test(req)
    
    // Assert status code
    assert.Equal(t, http.StatusOK, resp.StatusCode)
    
    // Parse response body
    body, _ := ioutil.ReadAll(resp.Body)
    var question models.Question
    json.Unmarshal(body, &question)
    
    // Assert response
    assert.Equal(t, "Test Question", question.Name)
    assert.Equal(t, "MCQ", question.Type)
    assert.Equal(t, 2, len(question.Choices))
}

The Complete System

Here's how all the components work together:

Complete System Flow

  1. Frontend (React) sends HTTP requests to the backend API
  2. Backend Router directs the request to the appropriate controller
  3. JWT Authentication Middleware verifies user credentials
  4. Controller processes the request and calls the service layer
  5. Service Layer implements business logic
  6. Repository Layer interacts with the database
  7. Database (PostgreSQL) stores and retrieves data
  8. Response flows back through the layers to the frontend

What I Learned

Developing the backend for Candidly taught me several valuable lessons:

  1. API Design: RESTful API design principles are crucial for a clean, maintainable API
  2. Authentication: JWT provides a secure, stateless authentication mechanism
  3. Clean Architecture: Separating concerns into controllers, services, and repositories improves maintainability
  4. Documentation: Swagger makes API documentation a breeze
  5. Testing: Comprehensive testing ensures reliability

Conclusion

Over these six weeks, we've built a complete interview assessment platform from planning to implementation. The backend, built with Golang, ties everything together, providing a robust API for the frontend to consume.

The combination of Golang's performance, the Fiber framework's simplicity, and Swagger's documentation capabilities make for a powerful backend stack that can handle complex business logic while remaining maintainable and scalable.

This concludes our learning series on building a complete web application. I hope you've gained valuable insights into modern web development practices. Feel free to explore the repositories and continue learning!


GitHub Repositories

Project Resources

image.png
Authentication interface
image.png
Question type selection interface
image.png
Interface for adding questions to a test
image.png
Interface for adding an MCQ-type question
image.png
Interface for inviting a candidate to a test
image.png
Candidate welcome interface


What can you share in the club?

Our club is all about technology and development including:

  • Web & Mobile Development
  • AI & Machine Learning
  • DevOps & Cloud Technologies
  • Blockchain & Decentralized Applications
  • Open-source Contributions
  • Graphic Design & UI/UX

Any posts related to technology, reviews, information, tips, and practical experience must include original pictures, real-life reviews of the product, the changes it has brought to you, and a demonstration of practical experience

The club is open to everyone. Even if you're not interested in development, you can still share ideas for projects, and maybe someone will take the initiative to work on them and collaborate with you. Don't worry if you don't have much IT knowledge, just share your great ideas with us and provide feedback on the development projects. For example, if I create an extension, you can give your feedback as a user, test the specific project, and that will make you an important part of our club. We encourage people to talk and share posts and ideas related to Steemit.

For more information about the #techclub you can go through A New Era of Learning on Steemit: Join the Technology and Development Club. It has all the details about posting in the "#techclub" and if you have any doubts or needs clarification you can ask.


Our Club Rules

To ensure a fair and valuable experience, we have a few rules:

  1. No AI generated content. We want real human creativity and effort.
  2. Respect each other. This is a learning and collaborative space.
  3. No simple open source projects. If you submit an open source project, ensure you add significant modifications or improvements and share the source of the open source.
  4. Project code must be hosted in a Git repository (GitHub/GitLab preferred). If privacy is a concern, provide limited access to our mentors.
  5. Any post in our club that is published with the main tag "#techclub" please mention the mentors @kafio @mohammadfaisal @alejos7ven
  6. Use the tag #techclub, #techclub-s23w6, "#country", other specific tags relevant to your post.
  7. In this week's "#techclub" you can participate from Monday, March 24, 2025, 00:00 UTC to Sunday, March 30, 2025, 23:59 UTC.
  8. Post the link to your entry in the comments section of this contest post. (Must)
  9. Invite at least 3 friends to participate.
  10. Try to leave valuable feedback on other people's entries.
  11. Share your post on Twitter and drop the link as a comment on your post.

Each post will be reviewed according to the working schedule as a major review which will have all the information about the post such as AI, Plagiarism, Creativity, Originality and suggestions for the improvements.

Other team members will also try to provide their suggestions just like a simple user but the major review will be one which will have all the details.


Rewards System

Sc01 and Sc02 will be visiting the posts of the users and participating teaching teams and learning clubs and upvoting outstanding content. Upvote is not guaranteed for all articles. Kindly take note.

Each week we will select Top 4 posts which has outperformed from others while maintaining the quality of their entry, interaction with other users, unique ideas, and creativity. These top 4 posts will receive additional winning rewards from SC01/SC02.

Note: If we find any valuable and outstanding comment than the post then we can select that comment as well instead of the post.


Best Regards
Technology and Development Club Team


cc:@steemcurator01

Sort:  

Upvoted! Thank you for supporting witness @jswit.