Skip to main content

Gin Resource Routing

In this tutorial, we'll explore how to implement resource routing in Gin, a popular Go web framework. Resource routing is a pattern that helps you organize your API endpoints around resources in a RESTful manner, making your API more intuitive, maintainable, and following industry best practices.

Introduction to Resource Routing

Resource routing is a concept from REST (Representational State Transfer) architecture that treats application data as resources that can be manipulated using standard HTTP methods. For example, a "user" resource might have endpoints for creating, reading, updating, and deleting user data, corresponding to HTTP methods: POST, GET, PUT/PATCH, and DELETE.

In Gin, we can implement this pattern by organizing our routes and handlers in a way that follows RESTful conventions.

Basic Resource Routing Structure

Let's set up a basic structure for resource routing in Gin:

go
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
router := gin.Default()

// User resource routes
users := router.Group("/users")
{
users.GET("/", listUsers)
users.GET("/:id", getUser)
users.POST("/", createUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}

router.Run(":8080")
}

// Handler functions
func listUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "List all users",
})
}

func getUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"message": "Get user details",
"id": id,
})
}

func createUser(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{
"message": "Create a new user",
})
}

func updateUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"message": "Update user details",
"id": id,
})
}

func deleteUser(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"message": "Delete user",
"id": id,
})
}

This example sets up basic CRUD (Create, Read, Update, Delete) operations for a user resource.

Organizing Routes by Resource

For a more complex API with multiple resources, you can organize your routes like this:

go
func setupRoutes(router *gin.Engine) {
// User resource
users := router.Group("/users")
{
users.GET("/", listUsers)
users.GET("/:id", getUser)
users.POST("/", createUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}

// Product resource
products := router.Group("/products")
{
products.GET("/", listProducts)
products.GET("/:id", getProduct)
products.POST("/", createProduct)
products.PUT("/:id", updateProduct)
products.DELETE("/:id", deleteProduct)
}

// Order resource
orders := router.Group("/orders")
{
orders.GET("/", listOrders)
orders.GET("/:id", getOrder)
orders.POST("/", createOrder)
orders.PUT("/:id", updateOrder)
orders.DELETE("/:id", deleteOrder)

// Nested resource: order items
orderItems := orders.Group("/:orderId/items")
{
orderItems.GET("/", listOrderItems)
orderItems.POST("/", addOrderItem)
orderItems.DELETE("/:itemId", removeOrderItem)
}
}
}

Structuring a Resource Controller

To better organize our code, let's implement a resource controller pattern for our user resource:

go
// UserController handles user resource operations
type UserController struct {
// Dependencies like services or repositories would go here
}

// NewUserController creates a new user controller
func NewUserController() *UserController {
return &UserController{}
}

// RegisterRoutes registers all user routes
func (uc *UserController) RegisterRoutes(router *gin.RouterGroup) {
users := router.Group("/users")
{
users.GET("/", uc.List)
users.GET("/:id", uc.Get)
users.POST("/", uc.Create)
users.PUT("/:id", uc.Update)
users.DELETE("/:id", uc.Delete)
}
}

// List returns all users
func (uc *UserController) List(c *gin.Context) {
// Implementation...
c.JSON(http.StatusOK, gin.H{"message": "List all users"})
}

// Get returns a specific user
func (uc *UserController) Get(c *gin.Context) {
id := c.Param("id")
// Implementation...
c.JSON(http.StatusOK, gin.H{"message": "Get user details", "id": id})
}

// Create adds a new user
func (uc *UserController) Create(c *gin.Context) {
// Implementation...
c.JSON(http.StatusCreated, gin.H{"message": "Create a new user"})
}

// Update modifies an existing user
func (uc *UserController) Update(c *gin.Context) {
id := c.Param("id")
// Implementation...
c.JSON(http.StatusOK, gin.H{"message": "Update user details", "id": id})
}

// Delete removes a user
func (uc *UserController) Delete(c *gin.Context) {
id := c.Param("id")
// Implementation...
c.JSON(http.StatusOK, gin.H{"message": "Delete user", "id": id})
}

And in your main function:

go
func main() {
router := gin.Default()

// Initialize controllers
userController := NewUserController()

// Register routes
api := router.Group("/api/v1")
userController.RegisterRoutes(api)

router.Run(":8080")
}

Practical Example: Blog API

Let's implement a practical example with a more complete blog API that has posts and comments:

go
package main

import (
"github.com/gin-gonic/gin"
"net/http"
)

func main() {
router := gin.Default()
api := router.Group("/api/v1")

// Initialize and register controllers
postController := NewPostController()
postController.RegisterRoutes(api)

router.Run(":8080")
}

// Post model
type Post struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Author string `json:"author"`
}

// PostController handles post resource operations
type PostController struct {
// In a real app, you would have a service or repository here
posts []Post
}

// NewPostController creates a new post controller with some sample data
func NewPostController() *PostController {
return &PostController{
posts: []Post{
{ID: "1", Title: "First Post", Content: "Hello world!", Author: "John"},
{ID: "2", Title: "Go Programming", Content: "Gin is awesome", Author: "Jane"},
},
}
}

// RegisterRoutes registers all post routes
func (pc *PostController) RegisterRoutes(router *gin.RouterGroup) {
posts := router.Group("/posts")
{
posts.GET("/", pc.ListPosts)
posts.GET("/:id", pc.GetPost)
posts.POST("/", pc.CreatePost)
posts.PUT("/:id", pc.UpdatePost)
posts.DELETE("/:id", pc.DeletePost)

// Nested comments resource
comments := posts.Group("/:postId/comments")
{
comments.GET("/", pc.ListComments)
comments.POST("/", pc.AddComment)
comments.DELETE("/:commentId", pc.DeleteComment)
}
}
}

// ListPosts returns all posts
func (pc *PostController) ListPosts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": pc.posts,
})
}

// GetPost returns a specific post
func (pc *PostController) GetPost(c *gin.Context) {
id := c.Param("id")

for _, post := range pc.posts {
if post.ID == id {
c.JSON(http.StatusOK, gin.H{
"data": post,
})
return
}
}

c.JSON(http.StatusNotFound, gin.H{
"error": "Post not found",
})
}

// CreatePost adds a new post
func (pc *PostController) CreatePost(c *gin.Context) {
var newPost Post
if err := c.ShouldBindJSON(&newPost); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// In a real app, we would generate an ID and save to database
newPost.ID = "3" // Simplified for example
pc.posts = append(pc.posts, newPost)

c.JSON(http.StatusCreated, gin.H{
"message": "Post created successfully",
"data": newPost,
})
}

// UpdatePost modifies an existing post
func (pc *PostController) UpdatePost(c *gin.Context) {
id := c.Param("id")
var updatedPost Post

if err := c.ShouldBindJSON(&updatedPost); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

for i, post := range pc.posts {
if post.ID == id {
updatedPost.ID = id
pc.posts[i] = updatedPost
c.JSON(http.StatusOK, gin.H{
"message": "Post updated successfully",
"data": updatedPost,
})
return
}
}

c.JSON(http.StatusNotFound, gin.H{
"error": "Post not found",
})
}

// DeletePost removes a post
func (pc *PostController) DeletePost(c *gin.Context) {
id := c.Param("id")

for i, post := range pc.posts {
if post.ID == id {
// Remove the post from slice
pc.posts = append(pc.posts[:i], pc.posts[i+1:]...)
c.JSON(http.StatusOK, gin.H{
"message": "Post deleted successfully",
})
return
}
}

c.JSON(http.StatusNotFound, gin.H{
"error": "Post not found",
})
}

// Comment related handlers (simplified)
func (pc *PostController) ListComments(c *gin.Context) {
postId := c.Param("postId")
c.JSON(http.StatusOK, gin.H{
"message": "List comments for post",
"postId": postId,
})
}

func (pc *PostController) AddComment(c *gin.Context) {
postId := c.Param("postId")
c.JSON(http.StatusCreated, gin.H{
"message": "Comment added",
"postId": postId,
})
}

func (pc *PostController) DeleteComment(c *gin.Context) {
postId := c.Param("postId")
commentId := c.Param("commentId")
c.JSON(http.StatusOK, gin.H{
"message": "Comment deleted",
"postId": postId,
"commentId": commentId,
})
}

Testing Resource Routes

We can test our resource endpoints using tools like cURL or Postman. Here's an example of testing our blog API:

List all posts:

bash
curl -X GET http://localhost:8080/api/v1/posts

Response:

json
{
"data": [
{"id":"1","title":"First Post","content":"Hello world!","author":"John"},
{"id":"2","title":"Go Programming","content":"Gin is awesome","author":"Jane"}
]
}

Get a specific post:

bash
curl -X GET http://localhost:8080/api/v1/posts/1

Response:

json
{
"data": {"id":"1","title":"First Post","content":"Hello world!","author":"John"}
}

Create a new post:

bash
curl -X POST http://localhost:8080/api/v1/posts \
-H "Content-Type: application/json" \
-d '{"title":"New Post","content":"This is a new post","author":"Alice"}'

Response:

json
{
"message": "Post created successfully",
"data": {"id":"3","title":"New Post","content":"This is a new post","author":"Alice"}
}

Best Practices for Resource Routing in Gin

  1. Use proper HTTP methods:

    • GET for retrieving resources
    • POST for creating new resources
    • PUT/PATCH for updating resources
    • DELETE for removing resources
  2. Organize routes into logical resource groups using router.Group().

  3. Use descriptive resource-oriented URLs:

    • /posts for post collection
    • /posts/:id for a specific post
  4. Implement proper HTTP status codes:

    • 200 OK for successful GET, PUT, or DELETE
    • 201 Created for successful POST
    • 400 Bad Request for client errors
    • 404 Not Found for resources that don't exist
    • 500 Server Error for server-side issues
  5. Implement pagination for list endpoints to handle large collections.

  6. Add versioning to your API (e.g., /api/v1/posts) to support future changes.

  7. Use controllers to organize related handlers for each resource.

Summary

Resource routing in Gin provides a clean and organized way to structure your RESTful APIs. By grouping related endpoints around resources and following RESTful conventions, you can create APIs that are intuitive, maintainable, and follow industry best practices.

In this tutorial, we've covered:

  • Basic resource routing structure in Gin
  • Organizing routes by resource
  • Implementing a resource controller pattern
  • Building a practical blog API example
  • Testing resource endpoints
  • Best practices for resource routing

With these patterns and techniques, you can build scalable and maintainable APIs using the Gin framework.

Additional Resources and Exercises

Resources to Learn More

Exercises

  1. Extend the blog API with a "categories" resource and implement CRUD operations for it.
  2. Add proper validation for the request payloads in the blog API.
  3. Implement pagination for the ListPosts endpoint.
  4. Add authentication middleware to protect the create, update, and delete routes.
  5. Implement proper error handling and custom error responses.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)