Creating a High-Performance Furniture Website Backend with Go's Standard Library

Harness the power of Go's standard library to create a robust web backend for a furniture store.

Introduction

Go, often referred to as Golang, is renowned for its simplicity, performance, and powerful standard library. While many web applications leverage frameworks like Gin or Echo, Go's standard library provides all the essential tools to build a fully functional web backend. In this blog post, we'll explore how to create an advanced furniture web backend using only Go's standard library.

Table of Contents

  1. Prerequisites

  2. Project Structure

  3. Defining the Data Model

  4. Setting Up the In-Memory Data Store

  5. Implementing the HTTP Handlers

  6. Starting the HTTP Server

  7. Testing the API

  8. Advanced Features and Considerations

  9. Conclusion


Prerequisites

  • Basic understanding of Go programming language.

  • Go installed on your machine (version 1.16 or later recommended).

  • Familiarity with HTTP concepts and RESTful API design.

Project Structure

For simplicity, we'll keep all our code in a single main.go file. In a production environment, you would organize your code into packages and files for better maintainability.

furniture-backend/
└── main.go

Defining the Data Model

First, we define a Furniture struct to represent our furniture items.

type Furniture struct {
    ID       int     `json:"id"`
    Name     string  `json:"name"`
    Type     string  `json:"type"`
    Material string  `json:"material"`
    Price    float64 `json:"price"`
}

The struct fields are tagged with json annotations to facilitate JSON serialization and deserialization.

Setting Up the In-Memory Data Store

We'll use a map to store our furniture items and a mutex to handle concurrent access.

var (
    furnitureStore = make(map[int]Furniture)
    idCounter      = 1
    storeMutex     sync.Mutex
)
  • furnitureStore: Holds the furniture items with their IDs as keys.

  • idCounter: Generates unique IDs for new furniture items.

  • storeMutex: Ensures thread-safe operations on the data store.

Implementing the HTTP Handlers

Retrieving All Furniture Items

func getFurnitures(w http.ResponseWriter, r *http.Request) {
    storeMutex.Lock()
    defer storeMutex.Unlock()

    var furnitures []Furniture
    for _, item := range furnitureStore {
        furnitures = append(furnitures, item)
    }

    jsonResponse(w, furnitures, http.StatusOK)
}
  • Locks the store to prevent concurrent access issues.

  • Collects all furniture items into a slice.

  • Sends the slice as a JSON response with a 200 OK status.

Getting a Furniture Item by ID

func getFurnitureByID(w http.ResponseWriter, r *http.Request) {
    idStr := r.URL.Query().Get("id")
    if idStr == "" {
        http.Error(w, "Missing 'id' query parameter", http.StatusBadRequest)
        return
    }

    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid 'id' parameter", http.StatusBadRequest)
        return
    }

    storeMutex.Lock()
    furniture, exists := furnitureStore[id]
    storeMutex.Unlock()

    if !exists {
        http.Error(w, "Furniture not found", http.StatusNotFound)
        return
    }

    jsonResponse(w, furniture, http.StatusOK)
}
  • Retrieves the id from the query parameters.

  • Validates the id and fetches the corresponding furniture item.

  • Returns the item as a JSON response or an error if not found.

Creating a New Furniture Item

func createFurniture(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var newFurniture Furniture
    if err := json.NewDecoder(r.Body).Decode(&newFurniture); err != nil {
        http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
        return
    }

    storeMutex.Lock()
    newFurniture.ID = idCounter
    furnitureStore[idCounter] = newFurniture
    idCounter++
    storeMutex.Unlock()

    jsonResponse(w, newFurniture, http.StatusCreated)
}
  • Checks that the request method is POST.

  • Decodes the JSON payload into a Furniture struct.

  • Assigns a new ID and stores the item.

  • Returns the created item with a 201 Created status.

Updating an Existing Furniture Item

func updateFurniture(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPut {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var updatedFurniture Furniture
    if err := json.NewDecoder(r.Body).Decode(&updatedFurniture); err != nil {
        http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
        return
    }

    storeMutex.Lock()
    defer storeMutex.Unlock()

    if _, exists := furnitureStore[updatedFurniture.ID]; !exists {
        http.Error(w, "Furniture not found", http.StatusNotFound)
        return
    }

    furnitureStore[updatedFurniture.ID] = updatedFurniture
    jsonResponse(w, updatedFurniture, http.StatusOK)
}
  • Ensures the request method is PUT.

  • Updates the existing furniture item if it exists.

  • Returns the updated item with a 200 OK status.

Deleting a Furniture Item

func deleteFurniture(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodDelete {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    idStr := r.URL.Query().Get("id")
    if idStr == "" {
        http.Error(w, "Missing 'id' query parameter", http.StatusBadRequest)
        return
    }

    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid 'id' parameter", http.StatusBadRequest)
        return
    }

    storeMutex.Lock()
    defer storeMutex.Unlock()

    if _, exists := furnitureStore[id]; !exists {
        http.Error(w, "Furniture not found", http.StatusNotFound)
        return
    }

    delete(furnitureStore, id)
    w.WriteHeader(http.StatusNoContent)
}
  • Verifies the request method is DELETE.

  • Deletes the specified furniture item if it exists.

  • Returns a 204 No Content status on success.

Starting the HTTP Server

func main() {
    // Initial data
    furnitureStore[1] = Furniture{ID: 1, Name: "Armchair", Type: "Seating", Material: "Leather", Price: 249.99}
    furnitureStore[2] = Furniture{ID: 2, Name: "Dining Table", Type: "Table", Material: "Wood", Price: 549.99}
    idCounter = 3

    // Route handlers
    http.HandleFunc("/furnitures", getFurnitures)
    http.HandleFunc("/furnitures/get", getFurnitureByID)
    http.HandleFunc("/furnitures/create", createFurniture)
    http.HandleFunc("/furnitures/update", updateFurniture)
    http.HandleFunc("/furnitures/delete", deleteFurniture)

    // Start the server
    log.Println("Starting server on :8080...")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
  • Initializes the data store with sample furniture items.

  • Registers the HTTP handlers with their respective routes.

  • Starts the HTTP server on port 8080.

Testing the API

1. Get All Furnitures

Request:

GET http://localhost:8080/furnitures

Response:

[
  {
    "id": 1,
    "name": "Armchair",
    "type": "Seating",
    "material": "Leather",
    "price": 249.99
  },
  {
    "id": 2,
    "name": "Dining Table",
    "type": "Table",
    "material": "Wood",
    "price": 549.99
  }
]

2. Get Furniture by ID

Request:

GET http://localhost:8080/furnitures/get?id=1

Response:

{
  "id": 1,
  "name": "Armchair",
  "type": "Seating",
  "material": "Leather",
  "price": 249.99
}

3. Create New Furniture

Request:

POST http://localhost:8080/furnitures/create
Content-Type: application/json

{
  "name": "Bookshelf",
  "type": "Storage",
  "material": "Wood",
  "price": 199.99
}

Response:

{
  "id": 3,
  "name": "Bookshelf",
  "type": "Storage",
  "material": "Wood",
  "price": 199.99
}

4. Update Furniture

Request:

PUT http://localhost:8080/furnitures/update
Content-Type: application/json

{
  "id": 2,
  "name": "Dining Table",
  "type": "Table",
  "material": "Glass",
  "price": 599.99
}

Response:

{
  "id": 2,
  "name": "Dining Table",
  "type": "Table",
  "material": "Glass",
  "price": 599.99
}

5. Delete Furniture

Request:

DELETE http://localhost:8080/furnitures/delete?id=1

Response:

Status Code: 204 No Content

Advanced Features and Considerations

While the basic CRUD operations are functional, there are several advanced features and best practices to consider for a production-ready application.

Data Persistence

Using an in-memory store means all data is lost when the application stops. Integrate a database to persist data:

  • SQL Databases: Use database/sql with drivers like pq for PostgreSQL or mysql for MySQL.

  • NoSQL Databases: Interface with databases like MongoDB using appropriate drivers.

Input Validation

Ensure that all incoming data meets required criteria to prevent errors and security vulnerabilities.

func validateFurniture(f Furniture) error {
    if f.Name == "" || f.Type == "" || f.Material == "" {
        return errors.New("Name, Type, and Material are required")
    }
    if f.Price <= 0 {
        return errors.New("Price must be greater than zero")
    }
    return nil
}

Use this validation function before processing data in your handlers.

Authentication and Authorization

Implement authentication mechanisms to secure your API:

  • Token-Based Authentication: Use JWT tokens to authenticate users.

  • API Keys: Assign API keys to clients for access control.

Middleware

Create middleware functions to handle cross-cutting concerns:

  • Logging Middleware: Log requests and responses for monitoring.

  • CORS Middleware: Handle Cross-Origin Resource Sharing if your API will be accessed from browsers.

  • Recovery Middleware: Gracefully handle panics and prevent server crashes.

Pagination and Filtering

Enhance the getFurnitures handler to support pagination and filtering:

func getFurnitures(w http.ResponseWriter, r *http.Request) {
    // Parse query parameters for pagination and filtering
}

Error Handling

Standardize your error responses to provide consistent feedback to clients.

type ErrorResponse struct {
    Error string `json:"error"`
}

Use this struct to send error messages in JSON format.

Conclusion

Building a web backend using Go's standard library is both straightforward and efficient. By leveraging the powerful packages provided out-of-the-box, you can create robust and high-performance applications without external dependencies.

This furniture web backend serves as a foundational example. From here, you can expand its capabilities by integrating databases, implementing security features, and enhancing user experience.

Happy Coding!

0
Subscribe to my newsletter

Read articles from Sundaram Kumar Jha directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sundaram Kumar Jha
Sundaram Kumar Jha

I Like Building Cloud Native Stuff , the microservices, backends, distributed systemsand cloud native tools using Golang