Gin Nested Routes
When building web applications with Gin, organizing your routes becomes increasingly important as your application grows. Gin provides a powerful feature called nested routes or route groups that helps you structure your API endpoints logically and maintain cleaner code. This tutorial will guide you through using nested routes in Gin to create well-organized web applications.
What Are Nested Routes?
Nested routes allow you to group related endpoints under a common path prefix. This is especially useful when:
- Creating versioned APIs (like
/v1/users
,/v2/users
) - Organizing resources hierarchically (like
/users/:id/posts
) - Applying middleware to a specific set of routes
- Keeping your router code clean and modular
Basic Route Grouping
Let's start with a simple example of route grouping in Gin:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// Create a group for API routes
api := router.Group("/api")
{
// These routes will be prefixed with /api
api.GET("/users", getUsers)
api.GET("/products", getProducts)
}
router.Run(":8080")
}
func getUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Getting all users",
})
}
func getProducts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Getting all products",
})
}
In this example, both routes are prefixed with /api
, resulting in:
/api/users
- Returns user data/api/products
- Returns product data
Nested Groups
You can create multiple levels of nested routes by grouping within groups:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// API version 1 group
v1 := router.Group("/v1")
{
users := v1.Group("/users")
{
users.GET("", getAllUsers)
users.GET("/:id", getUserByID)
// Nested route for user posts
posts := users.Group("/:id/posts")
{
posts.GET("", getUserPosts)
posts.POST("", createUserPost)
posts.GET("/:postID", getUserPost)
}
}
products := v1.Group("/products")
{
products.GET("", getAllProducts)
products.GET("/:id", getProductByID)
}
}
router.Run(":8080")
}
func getAllUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Get all users"})
}
func getUserByID(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "Get user with ID: " + id})
}
func getUserPosts(c *gin.Context) {
userID := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "Get all posts for user: " + userID})
}
func createUserPost(c *gin.Context) {
userID := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "Create a post for user: " + userID})
}
func getUserPost(c *gin.Context) {
userID := c.Param("id")
postID := c.Param("postID")
c.JSON(http.StatusOK, gin.H{
"message": "Get post " + postID + " for user: " + userID,
})
}
func getAllProducts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Get all products"})
}
func getProductByID(c *gin.Context) {
id := c.Param("id")
c.JSON(http.StatusOK, gin.H{"message": "Get product with ID: " + id})
}
With this setup, we've created several nested routes:
/v1/users
- Get all users/v1/users/:id
- Get a specific user/v1/users/:id/posts
- Get all posts for a specific user/v1/users/:id/posts/:postID
- Get a specific post for a specific user/v1/products
- Get all products/v1/products/:id
- Get a specific product
Applying Middleware to Route Groups
One of the most powerful features of route groups is the ability to apply middleware to an entire group of routes:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"log"
)
func main() {
router := gin.Default()
// Public routes
public := router.Group("/public")
{
public.GET("/products", getPublicProducts)
}
// Protected routes with authentication middleware
authenticated := router.Group("/admin")
authenticated.Use(authMiddleware())
{
authenticated.GET("/users", getAdminUsers)
authenticated.GET("/analytics", getAdminAnalytics)
}
router.Run(":8080")
}
func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token != "valid-token" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
c.Abort()
return
}
// Continue to the next handler if authenticated
c.Next()
}
}
func getPublicProducts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Public products data"})
}
func getAdminUsers(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Admin users data"})
}
func getAdminAnalytics(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Admin analytics data"})
}
In this example:
- The
/public/products
endpoint is accessible to everyone - The
/admin/users
and/admin/analytics
endpoints require authentication
Testing this API:
GET /public/products
will return the products data without authenticationGET /admin/users
without an Authorization header will return a 401 Unauthorized errorGET /admin/users
withAuthorization: valid-token
header will return the admin users data
Real-world Example: E-commerce API
Let's implement a more comprehensive example of an e-commerce API with nested routes:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
router := gin.Default()
// API versioning
v1 := router.Group("/api/v1")
{
// Auth endpoints
auth := v1.Group("/auth")
{
auth.POST("/register", register)
auth.POST("/login", login)
auth.POST("/forgot-password", forgotPassword)
}
// User endpoints with authentication
users := v1.Group("/users")
users.Use(authRequired())
{
users.GET("/profile", getUserProfile)
users.PUT("/profile", updateUserProfile)
// User addresses
addresses := users.Group("/addresses")
{
addresses.GET("", getUserAddresses)
addresses.POST("", addUserAddress)
addresses.PUT("/:addressID", updateUserAddress)
addresses.DELETE("/:addressID", deleteUserAddress)
}
// User orders
orders := users.Group("/orders")
{
orders.GET("", getUserOrders)
orders.GET("/:orderID", getUserOrderDetails)
orders.POST("", createUserOrder)
}
}
// Products endpoints (public)
products := v1.Group("/products")
{
products.GET("", getProducts)
products.GET("/:id", getProductDetails)
// Product reviews
reviews := products.Group("/:id/reviews")
{
reviews.GET("", getProductReviews)
reviews.POST("", authRequired(), addProductReview)
}
}
}
router.Run(":8080")
}
// Authentication middleware
func authRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
// Simple token validation for demo purposes
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
c.Abort()
return
}
// In real application, validate JWT token here
c.Set("userID", "user-123") // Set user context after authentication
c.Next()
}
}
// Auth handlers
func register(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "User registered successfully"})
}
func login(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"token": "sample-jwt-token"})
}
func forgotPassword(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Password reset email sent"})
}
// User handlers
func getUserProfile(c *gin.Context) {
userID, _ := c.Get("userID")
c.JSON(http.StatusOK, gin.H{
"id": userID,
"name": "John Doe",
"email": "[email protected]",
})
}
func updateUserProfile(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Profile updated successfully"})
}
// Address handlers
func getUserAddresses(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"addresses": []gin.H{
{"id": "addr-1", "street": "123 Main St", "city": "Anytown"},
{"id": "addr-2", "street": "456 Oak Ave", "city": "Somewhere"},
},
})
}
func addUserAddress(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "Address added", "id": "addr-3"})
}
func updateUserAddress(c *gin.Context) {
addressID := c.Param("addressID")
c.JSON(http.StatusOK, gin.H{"message": "Address updated", "id": addressID})
}
func deleteUserAddress(c *gin.Context) {
addressID := c.Param("addressID")
c.JSON(http.StatusOK, gin.H{"message": "Address deleted", "id": addressID})
}
// Order handlers
func getUserOrders(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"orders": []gin.H{
{"id": "order-1", "total": 99.99, "status": "delivered"},
{"id": "order-2", "total": 49.99, "status": "processing"},
},
})
}
func getUserOrderDetails(c *gin.Context) {
orderID := c.Param("orderID")
c.JSON(http.StatusOK, gin.H{
"id": orderID,
"items": []gin.H{
{"product": "Product 1", "quantity": 2, "price": 24.99},
{"product": "Product 2", "quantity": 1, "price": 49.99},
},
"total": 99.97,
"status": "delivered",
})
}
func createUserOrder(c *gin.Context) {
c.JSON(http.StatusCreated, gin.H{"message": "Order created", "id": "order-3"})
}
// Product handlers
func getProducts(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"products": []gin.H{
{"id": "prod-1", "name": "Product 1", "price": 24.99},
{"id": "prod-2", "name": "Product 2", "price": 49.99},
},
})
}
func getProductDetails(c *gin.Context) {
productID := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"id": productID,
"name": "Product " + productID,
"description": "This is a detailed description for the product",
"price": 24.99,
})
}
// Review handlers
func getProductReviews(c *gin.Context) {
productID := c.Param("id")
c.JSON(http.StatusOK, gin.H{
"productID": productID,
"reviews": []gin.H{
{"id": "rev-1", "rating": 5, "comment": "Great product!"},
{"id": "rev-2", "rating": 4, "comment": "Good value for money"},
},
})
}
func addProductReview(c *gin.Context) {
productID := c.Param("id")
userID, _ := c.Get("userID")
c.JSON(http.StatusCreated, gin.H{
"message": "Review added for product " + productID + " by user " + userID.(string),
"id": "rev-3",
})
}
This comprehensive example demonstrates:
- API versioning using
/api/v1
- Multiple nested route groups for different resources
- Middleware applied to specific route groups
- Parameter handling in nested routes
- Authentication for protected routes
Best Practices for Nested Routes
When using nested routes in your Gin application, keep these guidelines in mind:
-
Don't nest too deeply: Extremely nested routes (more than 3-4 levels) can become hard to maintain and understand.
-
Keep URL paths RESTful: Follow REST conventions where appropriate:
/users
(GET) for listing users/users/:id
(GET) for getting a specific user/users/:id/orders
(GET) for listing a user's orders
-
Apply middleware strategically: Apply middleware at the appropriate group level to minimize code duplication.
-
Organize code by domain: Consider organizing your route groups by domain, not just by URL structure.
-
Use descriptive handler names: As your routes grow, descriptive handler function names become even more important.
Structuring Routes Across Multiple Files
As your application grows, it's good practice to split your route definitions across multiple files:
// main.go
package main
import (
"github.com/gin-gonic/gin"
"myapp/routes"
)
func main() {
router := gin.Default()
// Setup route groups
api := router.Group("/api/v1")
// Register routes from other files
routes.SetupAuthRoutes(api)
routes.SetupUserRoutes(api)
routes.SetupProductRoutes(api)
router.Run(":8080")
}
// routes/auth.go
package routes
import "github.com/gin-gonic/gin"
func SetupAuthRoutes(router *gin.RouterGroup) {
auth := router.Group("/auth")
{
auth.POST("/register", controllers.Register)
auth.POST("/login", controllers.Login)
auth.POST("/forgot-password", controllers.ForgotPassword)
}
}
// routes/user.go
package routes
import "github.com/gin-gonic/gin"
func SetupUserRoutes(router *gin.RouterGroup) {
users := router.Group("/users")
users.Use(middleware.AuthRequired())
{
users.GET("/profile", controllers.GetUserProfile)
// Add more user routes...
// You can even call sub-route setup functions
setupUserAddressRoutes(users)
setupUserOrderRoutes(users)
}
}
func setupUserAddressRoutes(router *gin.RouterGroup) {
addresses := router.Group("/addresses")
{
addresses.GET("", controllers.GetUserAddresses)
addresses.POST("", controllers.AddUserAddress)
// More address routes...
}
}
This pattern helps keep your code organized and maintainable as your application grows.
Summary
Gin's nested routes provide a powerful way to organize your web application's endpoints. By grouping related routes together, you can:
- Create cleaner, more maintainable code
- Apply middleware to specific groups of routes
- Structure your API in a logical, hierarchical way
- Build complex applications while keeping your code organized
As your application grows, using nested routes effectively becomes increasingly important for keeping your codebase maintainable and your API intuitive for clients.
Additional Resources
Exercises
-
Create a blog API with the following nested routes:
/api/v1/posts
- List all posts/api/v1/posts/:id
- Get a specific post/api/v1/posts/:id/comments
- List comments for a post/api/v1/users/:id/posts
- List posts by a specific user
-
Add middleware to your blog API that:
- Logs all requests
- Requires authentication for creating posts and comments
- Rate-limits requests to sensitive endpoints
-
Refactor the e-commerce API example to split routes into separate files following the pattern shown in the "Structuring Routes Across Multiple Files" section.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)