DEV Community

Cover image for Building a Simple RESTful API in Go for Managing Blog Posts
Fred
Fred

Posted on • Edited on

Building a Simple RESTful API in Go for Managing Blog Posts

Introduction

We know the best way to learn is to build stuff while learning, so I got into building a RESTful API to manage blog posts. Itโ€™s a simple project that lets you create, read, update, and delete postsโ€”basically, the classic CRUD operations. I chose Go because I'm familiar with it,๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜Ž๐Ÿ˜‚! It's a great language for this kind of project, and it makes the whole process smoother since I already know my way around it. Plus, Goโ€™s performance is top-notch, which makes it perfect for handling API requests quickly.

1. Overview

In this tutorial, weโ€™ll walk through the entire process of creating a simple RESTful API in Go to manage blog posts. The API will allow us to perform the basic CRUD operations: Create, Read, Update, and Delete posts. We'll structure the application with a few simple files to keep things clear.

2. Our Entry Point

FILE: main.go

package main

import (
    "log"
    "net/http"

    "your_project/route" // import your route package
)

func main() {
    r := route.NewRouter() // initialize the router
    log.Fatal(http.ListenAndServe(":8080", r)) // start the server
}

Enter fullscreen mode Exit fullscreen mode

In this file, we set up the HTTP server and the routes using the mux router. The server listens on port 8080, and we define all the routes inside the route.NewRouter() function.

3. Post Model

FILE: model.go

package model

type Post struct {
    Title   string
    Content string
    Tags    []string
    Author   string
}
Enter fullscreen mode Exit fullscreen mode

This Post struct is our model, which represents a blog post. It contains fields for Title, Content, Tags, and Author. Each post will be stored in memory with these fields.

4. Setting Up Routes for CRUD Operations

FILE: route.go

package route

import (
    "github.com/gorilla/mux"

    "your_project/handler"
    "your_project/middleware"
)

func NewRouter() *mux.Router {
    r := mux.NewRouter()
// create a post
    r.HandleFunc("/create", handler.CreatePost).Methods("POST")
// list the titles of all posts
    r.HandleFunc("/list", handler.ListPosts).Methods("GET")
// get a post by the post title
    r.HandleFunc("/post", handler.GetPostByTitle).Methods("GET")
// edit a post
    r.HandleFunc("/edit", handler.UpdatePost).Methods("PUT")
// delete a blog post
    r.HandleFunc("/delete", middleware.RequireAuth(handler.DeletePost)).Methods("DELETE")
    return r
}

Enter fullscreen mode Exit fullscreen mode

Here, we define all the routes for the CRUD operations:

  • Create Post: POST /create
  • List All Posts: GET /posts
  • Get Post by Title: GET /blogs
  • Update Post: PUT /edit
  • Delete Post: DELETE /delete

5. Implementing the Handlers

FILE: handler.go

package handler

import (
    "encoding/json"
    "fmt"
    "net/http"

    "your_project/model"
)
// a in-built memory database to store and retrieve our posts.
var allPosts = make(map[string]model.Post)
Enter fullscreen mode Exit fullscreen mode
  • Create Post(POST)
func CreatePost(w http.ResponseWriter, r *http.Request) {
    var post model.Post
    // read content from request body into a new decoder
    decoder := json.NewDecoder(r.Body)
    // decode content into our Post struct
    err := decoder.Decode(&post)
    if err != nil {
        http.Error(w, "Failed to decode request body", http.StatusInternalServerError)
        return
    }
    // check for unique post title
    _, ok := allPosts[post.Title]
    if ok {
        http.Error(w, "Post title already exists", http.StatusBadRequest)
        return
    }

    // append post to our memory
    allPosts[post.Title] = post

    // prints out structs with field names
    fmt.Fprintf(w, "%+v", post)
}
Enter fullscreen mode Exit fullscreen mode
  • Get Post(GET)
func ListPosts(w http.ResponseWriter, r *http.Request) {
    titles := []string{}

    for _, post := range allPosts {
        titles = append(titles, post.Title)
    }

    if len(titles) == 0 {
        http.Error(w, "no posts found", http.StatusNotFound)
    }

    json.NewEncoder(w).Encode(titles)
    // uncomment to print out structs with field names
    // fmt.Fprintf(w,"%+v",titles)
}
Enter fullscreen mode Exit fullscreen mode
  • Get Post by Title(GET)
func GetPostByTitle(w http.ResponseWriter, r *http.Request) {
    // retrieve title of post
    title := r.URL.Query().Get("title")
    if title == "" {
        http.Error(w, "Title is required", http.StatusBadRequest)
        return
    }

    // check title is present
    post, ok := allPosts[title]
    if !ok {
        http.Error(w, "Post not found", http.StatusNotFound)
        return

    }

    // encode the contents
    if err := json.NewEncoder(w).Encode(post); err != nil {
        http.Error(w, "Post not found", http.StatusInternalServerError)
        return
    }
}

Enter fullscreen mode Exit fullscreen mode
  • Update Post(PUT)
func UpdatePost(w http.ResponseWriter, r *http.Request) {
    // get title
    // check title is provided
    title := r.URL.Query().Get("title")
    if title == "" {
        http.Error(w, "Title is required", http.StatusBadRequest)
        return
    }

    // check if such post exists
    post, ok := allPosts[title]
    if !ok {
        http.Error(w, "BlogPost not found", http.StatusNotFound)
        return
    }
var updatedPost model.Post

    // read request body
    if err := json.NewDecoder(r.Body).Decode(&updatedPost); err != nil {
        http.Error(w, "Failed to decode request body", http.StatusBadRequest)
        return
    }

    // update post
    allPosts[title] = updatedPost
    post = updatedPost

    // return ok status
    w.WriteHeader(http.StatusOK)

    // return updated content
    json.NewEncoder(w).Encode(post)


}
Enter fullscreen mode Exit fullscreen mode
  • Delete Post(Delete)
func DeletePost(w http.ResponseWriter, r *http.Request) {
    title := r.URL.Query().Get("title")

    // retrieve post
    _, ok := allPosts[title]
    if !ok {
        http.Error(w, "No post with such title", http.StatusNotFound)
    }
       // deletes the post from the map.
    delete(allPosts, title)

    w.WriteHeader(http.StatusOK)
}

Enter fullscreen mode Exit fullscreen mode

These handler functions handle the logic for each CRUD operation. They read the request body (for POST and PUT requests), interact with the in-memory database (allPosts), and send responses accordingly.

6. Middleware (Authentication)

FILE: auth.go

package middleware

import "net/http"

func RequireAuth(f http.HandlerFunc) http.HandlerFunc {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // check if user is authenticated
        token := r.Header.Get("Authorization")
        if token != "Bearer Secret" {
            http.Error(w,"Unathorized", http.StatusUnauthorized)
            return
        }
        f.ServeHTTP(w,r)
    })
}
Enter fullscreen mode Exit fullscreen mode

This middleware function checks for a valid Authorization header. If the token isnโ€™t Bearer Secret, it returns an unauthorized response.

7.Run the Aplication

Run the following commands to initialize and tidy up dependencies:
run to install.

cd your_project
go mod init your_project
go mod tidy
Enter fullscreen mode Exit fullscreen mode

8. Testing our API

Once the API is up and running, you can test the endpoints using curl commands.

Run the server:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Using curl

Then, in another terminal window, use curl to test:

  • Create Post:
curl -X POST -H "Content-Type: application/json" -d '{"title":"My First Post", "content":"This is the content.", "author":"John Doe"}' http://localhost:8080/create
Enter fullscreen mode Exit fullscreen mode
  • Get Post by Post Title:
curl -X GET 'http://localhost:8080/post?title=My%20First%20Post'

Enter fullscreen mode Exit fullscreen mode
  • Get All Posts Titles:
curl http://localhost:8080/list
Enter fullscreen mode Exit fullscreen mode
  • Update Post
curl -X PUT -H "Content-Type: application/json" -d '{"Title":"My First Post", "Content":"Updated content.", "Author":"John Doe"}' 'http://localhost:8080/edit?title=My%20Blog%20Post'
Enter fullscreen mode Exit fullscreen mode
  • Delete Post
curl -X DELETE -H "Authorization: Bearer Secret" 'http://localhost:8080/delete?title=My%20First%20Post'
Enter fullscreen mode Exit fullscreen mode

Using Thunder Client

Thunder Client is a lightweight and powerful API testing tool, typically used within Visual Studio Code (VS Code). It offers a user-friendly interface to make API requests.

Check out how to use Thunder Client to make API requests :

Conclusion

And there you have itโ€”your very own RESTful API for managing blog posts, built with the power of Go! ๐Ÿš€. From creating posts to updating, deleting, and viewing them.
But waitโ€”Want to add user authentication, store data in a database, or make your API even more scalable? Go for it! ๐Ÿ”ฅ

Top comments (6)

Collapse
 
anoduor profile image
Antony Oduor

Great article. I found it helpful while implementing an API of my own, most especially, the testing part using curl.

Collapse
 
fredgitonga profile image
Fred

Glad it was helpful, Antony! Command-line curl truly embodies the developer spirit. ๐Ÿš€

Collapse
 
philip_zhang_854092d88473 profile image
Philip

Building a RESTful API in Go for managing blog posts is a great way to practice CRUD operations due to Go's performance and simplicity. Thunder Client, while useful for basic API testing, lacks advanced features like in-depth monitoring and automation. EchoAPI provides more robust tools for professional API testing and debugging, making it a superior choice for developers.

Collapse
 
fredgitonga profile image
Fred

thank you for the insights on EchoAPI, Philip. I will surely check it out

Collapse
 
fredgitonga profile image
Fred

thank you for the insights on EchoAPI, I will check it out๐Ÿ™‚

Collapse
 
ouma_ouma profile image
Ouma Godwin

Do an article on "curl"