Building a Go Fiber API with Google Firestore

Introduction

In this post, we’ll extend our knowledge of Go Fiber by integrating it with Google Firestore. We’ll build a simple API that performs CRUD operations on a ‘users’ collection, demonstrating how to combine the speed of Fiber with the flexibility of Firestore.

Prerequisites

  • Basic knowledge of Go programming and Fiber (refer to our previous post on Fiber routing)
  • Google Cloud account with Firestore enabled
  • Firestore credentials (service account key)

Setting Up the Project

First, let’s set up our project and import the necessary packages:

package main

import (
    "context"
    "log"
    "os"

    "cloud.google.com/go/firestore"
    firebase "firebase.google.com/go"
    "github.com/gofiber/fiber/v2"
    "google.golang.org/api/option"
)

var firestoreClient *firestore.Client

func main() {
    // Firestore setup will go here
    
    app := fiber.New()
    
    // Routes will be defined here
    
    log.Fatal(app.Listen(":3000"))
}

Connecting to Firestore

Let’s set up our Firestore client:

func initFirestore() {
    ctx := context.Background()
    sa := option.WithCredentialsFile("path/to/your/service-account-key.json")
    app, err := firebase.NewApp(ctx, nil, sa)
    if err != nil {
        log.Fatalf("Failed to create Firebase app: %v", err)
    }

    firestoreClient, err = app.Firestore(ctx)
    if err != nil {
        log.Fatalf("Failed to create Firestore client: %v", err)
    }
}

Call initFirestore() at the beginning of your main() function.

Defining our User Model

Let’s define a simple User struct:

type User struct {
    ID    string `json:"id" firestore:"-"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

Implementing CRUD Operations

Now, let’s implement our CRUD operations using Fiber and Firestore:

Create a User

func createUser(c *fiber.Ctx) error {
    user := new(User)
    if err := c.BodyParser(user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
    }

    doc, _, err := firestoreClient.Collection("users").Add(context.Background(), user)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create user"})
    }

    user.ID = doc.ID
    return c.JSON(user)
}

Get a User

func getUser(c *fiber.Ctx) error {
    id := c.Params("id")
    doc, err := firestoreClient.Collection("users").Doc(id).Get(context.Background())
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
    }

    var user User
    doc.DataTo(&user)
    user.ID = doc.Ref.ID
    return c.JSON(user)
}

Update a User

func updateUser(c *fiber.Ctx) error {
    id := c.Params("id")
    user := new(User)
    if err := c.BodyParser(user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
    }

    _, err := firestoreClient.Collection("users").Doc(id).Set(context.Background(), user)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update user"})
    }

    user.ID = id
    return c.JSON(user)
}

Delete a User

func deleteUser(c *fiber.Ctx) error {
    id := c.Params("id")
    _, err := firestoreClient.Collection("users").Doc(id).Delete(context.Background())
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to delete user"})
    }

    return c.SendString("User successfully deleted")
}

Setting Up Routes

Now let’s set up our routes in the main() function:

func main() {
    initFirestore()
    defer firestoreClient.Close()

    app := fiber.New()

    api := app.Group("/api")
    users := api.Group("/users")

    users.Post("/", createUser)
    users.Get("/:id", getUser)
    users.Put("/:id", updateUser)
    users.Delete("/:id", deleteUser)

    log.Fatal(app.Listen(":3000"))
}

Error Handling and Validation

In a production application, you’d want to add more robust error handling and input validation. Consider using a library like go-playground/validator for request validation.

Conclusion

By combining Go Fiber with Google Firestore, we’ve created a powerful, scalable API. This setup allows for rapid development and easy scaling, making it an excellent choice for modern web applications.

Canonical Example

Here’s a complete example that puts all of these concepts together:

package main

import (
    "context"
    "log"
    "os"

    "cloud.google.com/go/firestore"
    firebase "firebase.google.com/go"
    "github.com/gofiber/fiber/v2"
    "google.golang.org/api/option"
)

var firestoreClient *firestore.Client

type User struct {
    ID    string `json:"id" firestore:"-"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func initFirestore() {
    ctx := context.Background()
    sa := option.WithCredentialsFile("path/to/your/service-account-key.json")
    app, err := firebase.NewApp(ctx, nil, sa)
    if err != nil {
        log.Fatalf("Failed to create Firebase app: %v", err)
    }

    firestoreClient, err = app.Firestore(ctx)
    if err != nil {
        log.Fatalf("Failed to create Firestore client: %v", err)
    }
}

func createUser(c *fiber.Ctx) error {
    user := new(User)
    if err := c.BodyParser(user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
    }

    doc, _, err := firestoreClient.Collection("users").Add(context.Background(), user)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to create user"})
    }

    user.ID = doc.ID
    return c.JSON(user)
}

func getUser(c *fiber.Ctx) error {
    id := c.Params("id")
    doc, err := firestoreClient.Collection("users").Doc(id).Get(context.Background())
    if err != nil {
        return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User not found"})
    }

    var user User
    doc.DataTo(&user)
    user.ID = doc.Ref.ID
    return c.JSON(user)
}

func updateUser(c *fiber.Ctx) error {
    id := c.Params("id")
    user := new(User)
    if err := c.BodyParser(user); err != nil {
        return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Cannot parse JSON"})
    }

    _, err := firestoreClient.Collection("users").Doc(id).Set(context.Background(), user)
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to update user"})
    }

    user.ID = id
    return c.JSON(user)
}

func deleteUser(c *fiber.Ctx) error {
    id := c.Params("id")
    _, err := firestoreClient.Collection("users").Doc(id).Delete(context.Background())
    if err != nil {
        return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to delete user"})
    }

    return c.SendString("User successfully deleted")
}

func main() {
    initFirestore()
    defer firestoreClient.Close()

    app := fiber.New()

    api := app.Group("/api")
    users := api.Group("/users")

    users.Post("/", createUser)
    users.Get("/:id", getUser)
    users.Put("/:id", updateUser)
    users.Delete("/:id", deleteUser)

    log.Fatal(app.Listen(":3000"))
}

This example provides a complete API for managing users, storing their data in Firestore. Remember to replace "path/to/your/service-account-key.json" with the actual path to your Firestore service account key.

In a production environment, you’d want to add authentication, more comprehensive error handling, and perhaps some caching mechanisms to optimize performance.