In this tutorial, we’ll walk through the process of creating a RESTful API for a simple blog application using Go. We’ll be using the following technologies:

  1. Gin: A web framework for Go
  2. FerretDB: A MongoDB-compatible database
  3. oapi-codegen: A tool for generating Go server boilerplate from OpenAPI 3.0 specifications

Table of Contents

  1. Setting Up the Project
  2. Defining the API Specification
  3. Generating Server Code
  4. Implementing the Database Layer
  5. Implementing the API Handlers
  6. Running the Application
  7. Testing the API
  8. Conclusion

Setting Up the Project

First, let’s set up our Go project and install the necessary dependencies:

mkdir blog-api
cd blog-api
go mod init github.com/yourusername/blog-api
go get github.com/gin-gonic/gin
go get github.com/deepmap/oapi-codegen/cmd/oapi-codegen
go get github.com/FerretDB/FerretDB

Defining the API Specification

Create a file named api.yaml in your project root and define the OpenAPI 3.0 specification for our blog API:

openapi: 3.0.0
info:
  title: Blog API
  version: 1.0.0
paths:
  /posts:
    get:
      summary: List all posts
      responses:
        '200':
          description: Successful response
          content:
            application/json:    
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Post'
    post:
      summary: Create a new post
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPost'
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
  /posts/{id}:
    get:
      summary: Get a post by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
    put:
      summary: Update a post
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NewPost'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Post'
    delete:
      summary: Delete a post
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '204':
          description: Successful response

components:
  schemas:
    Post:
      type: object
      properties:
        id:
          type: string
        title:
          type: string
        content:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    NewPost:
      type: object
      required:
        - title
        - content
      properties:
        title:
          type: string
        content:
          type: string

Generating Server Code

Now, let’s use oapi-codegen to generate the server code based on our API specification:

oapi-codegen -package api api.yaml > api/api.go

This command will create a new directory called api and generate the api.go file containing the server interfaces and models.

Implementing the Database Layer

Create a new file called db/db.go to implement the database layer using FerretDB:

package db

import (
	"context"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)

type Post struct {
	ID        primitive.ObjectID `bson:"_id,omitempty"`
	Title     string             `bson:"title"`
	Content   string             `bson:"content"`
	CreatedAt time.Time          `bson:"createdAt"`
	UpdatedAt time.Time          `bson:"updatedAt"`
}

type DB struct {
	client *mongo.Client
	posts  *mongo.Collection
}

func NewDB(uri string) (*DB, error) {
	client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(uri))
	if err != nil {
		return nil, err
	}

	db := client.Database("blog")
	posts := db.Collection("posts")

	return &DB{
		client: client,
		posts:  posts,
	}, nil
}

func (db *DB) Close() error {
	return db.client.Disconnect(context.Background())
}

func (db *DB) CreatePost(title, content string) (*Post, error) {
	post := &Post{
		Title:     title,
		Content:   content,
		CreatedAt: time.Now(),
		UpdatedAt: time.Now(),
	}

	result, err := db.posts.InsertOne(context.Background(), post)
	if err != nil {
		return nil, err
	}

	post.ID = result.InsertedID.(primitive.ObjectID)
	return post, nil
}

func (db *DB) GetPost(id string) (*Post, error) {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return nil, err
	}

	var post Post
	err = db.posts.FindOne(context.Background(), bson.M{"_id": objectID}).Decode(&post)
	if err != nil {
		return nil, err
	}

	return &post, nil
}

func (db *DB) UpdatePost(id, title, content string) (*Post, error) {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return nil, err
	}

	update := bson.M{
		"$set": bson.M{
			"title":     title,
			"content":   content,
			"updatedAt": time.Now(),
		},
	}

	var post Post
	err = db.posts.FindOneAndUpdate(
		context.Background(),
		bson.M{"_id": objectID},
		update,
		options.FindOneAndUpdate().SetReturnDocument(options.After),
	).Decode(&post)

	if err != nil {
		return nil, err
	}

	return &post, nil
}

func (db *DB) DeletePost(id string) error {
	objectID, err := primitive.ObjectIDFromHex(id)
	if err != nil {
		return err
	}

	_, err = db.posts.DeleteOne(context.Background(), bson.M{"_id": objectID})
	return err
}

func (db *DB) ListPosts() ([]*Post, error) {
	cursor, err := db.posts.Find(context.Background(), bson.M{})
	if err != nil {
		return nil, err
	}
	defer cursor.Close(context.Background())

	var posts []*Post
	for cursor.Next(context.Background()) {
		var post Post
		if err := cursor.Decode(&post); err != nil {
			return nil, err
		}
		posts = append(posts, &post)
	}

	return posts, nil
}

Implementing the API Handlers

Create a new file called handlers/handlers.go to implement the API handlers:

package handlers

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/blog-api/api"
	"github.com/yourusername/blog-api/db"
)

type BlogAPI struct {
	db *db.DB
}

func NewBlogAPI(db *db.DB) *BlogAPI {
	return &BlogAPI{db: db}
}

func (b *BlogAPI) ListPosts(c *gin.Context) {
	posts, err := b.db.ListPosts()
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	apiPosts := make([]api.Post, len(posts))
	for i, post := range posts {
		apiPosts[i] = api.Post{
			Id:        post.ID.Hex(),
			Title:     post.Title,
			Content:   post.Content,
			CreatedAt: post.CreatedAt,
			UpdatedAt: post.UpdatedAt,
		}
	}

	c.JSON(http.StatusOK, apiPosts)
}

func (b *BlogAPI) CreatePost(c *gin.Context) {
	var newPost api.NewPost
	if err := c.ShouldBindJSON(&newPost); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	post, err := b.db.CreatePost(newPost.Title, newPost.Content)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	c.JSON(http.StatusCreated, api.Post{
		Id:        post.ID.Hex(),
		Title:     post.Title,
		Content:   post.Content,
		CreatedAt: post.CreatedAt,
		UpdatedAt: post.UpdatedAt,
	})
}

func (b *BlogAPI) GetPost(c *gin.Context) {
	id := c.Param("id")
	post, err := b.db.GetPost(id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}

	c.JSON(http.StatusOK, api.Post{
		Id:        post.ID.Hex(),
		Title:     post.Title,
		Content:   post.Content,
		CreatedAt: post.CreatedAt,
		UpdatedAt: post.UpdatedAt,
	})
}

func (b *BlogAPI) UpdatePost(c *gin.Context) {
	id := c.Param("id")
	var updatePost api.NewPost
	if err := c.ShouldBindJSON(&updatePost); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	post, err := b.db.UpdatePost(id, updatePost.Title, updatePost.Content)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}

	c.JSON(http.StatusOK, api.Post{
		Id:        post.ID.Hex(),
		Title:     post.Title,
		Content:   post.Content,
		CreatedAt: post.CreatedAt,
		UpdatedAt: post.UpdatedAt,
	})
}

func (b *BlogAPI) DeletePost(c *gin.Context) {
	id := c.Param("id")
	err := b.db.DeletePost(id)
	if err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "Post not found"})
		return
	}

	c.Status(http.StatusNoContent)
}

Running the Application

Create a new file called main.go in the project root to set up and run the application:

package main

import (
	"log"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/blog-api/api"
	"github.com/yourusername/blog-api/db"
	"github.com/yourusername/blog-api/handlers"
)

func main() {
	// Initialize the database connection
	database, err := db.NewDB("mongodb://localhost:27017")
	if err != nil {
		log.Fatalf("Failed to connect to the database: %v", err)
	}
	defer database.Close()

	// Create a new Gin router
	router := gin.Default()

	// Initialize the BlogAPI handlers
	blogAPI := handlers.NewBlogAPI(database)

	// Register the API routes
	api.RegisterHandlers(router, blogAPI)

	// Start the server
	log.Println("Starting server on :8080")
	if err := router.Run(":8080"); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

Testing the API

Now that we have our API up and running, let’s test it using curl commands:

  1. Create a new post:
curl -X POST -H "Content-Type: application/json" -d '{"title":"My First Post","content":"This is the content of my first post."}' http://localhost:8080/posts
  1. List all posts:
curl http://localhost:8080/posts
  1. Get a specific post (replace {id} with the actual post ID):
curl http://localhost:8080/posts/{id}
  1. Update a post (replace {id} with the actual post ID):
curl -X PUT -H "Content-Type: application/json" -d '{"title":"Updated Post","content":"This is the updated content."}' http://localhost:8080/posts/{id}
  1. Delete a post (replace {id} with the actual post ID):
curl -X DELETE http://localhost:8080/posts/{id}

Conclusion

In this tutorial, we’ve built a simple blog API using the Gin framework, FerretDB, and oapi-codegen. We’ve covered the following steps:

  1. Setting up the project and installing dependencies
  2. Defining the API specification using OpenAPI 3.0
  3. Generating server code with oapi-codegen
  4. Implementing the database layer using FerretDB
  5. Implementing the API handlers
  6. Running the application
  7. Testing the API with curl commands

This project demonstrates how to create a RESTful API with Go, leveraging the power of code generation and a MongoDB-compatible database. You can further extend this API by adding authentication, pagination, and more complex querying capabilities.

Remember to handle errors appropriately, add proper logging, and implement security measures before deploying this API to a production environment.


Need Help?

Are you facing challenging problems, or need an external perspective on a new idea or project? I can help! Whether you're looking to build a technology proof of concept before making a larger investment, or you need guidance on difficult issues, I'm here to assist.

Services Offered:

  • Problem-Solving: Tackling complex issues with innovative solutions.
  • Consultation: Providing expert advice and fresh viewpoints on your projects.
  • Proof of Concept: Developing preliminary models to test and validate your ideas.

If you're interested in working with me, please reach out via email at [email protected].

Let's turn your challenges into opportunities!