Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature Request] [Fiber Framework] A Better Versioning REST APIs/Web Front End #225

Closed
1 task done
H0llyW00dzZ opened this issue Apr 8, 2024 · 3 comments
Closed
1 task done
Labels
enhancement New feature or request

Comments

@H0llyW00dzZ
Copy link
Contributor

Tell us about your feature request

As there is no implementation for better versioning of REST APIs/Web Front End for the Fiber framework, if anyone agrees, I can submit a pull request. This will also make it easier for maintainability, reusability, and scalability, following the idiomatic Go style.

Example:

// APIRoute represents a single API route, containing the path, HTTP method,
// handler function, and an optional rate limiter.
type APIRoute struct {
    Path        string
    Method      string
    Handler     fiber.Handler
    RateLimiter fiber.Handler
}

// APIGroup represents a group of API routes under a common prefix.
// It also allows for a group-wide rate limiter.
type APIGroup struct {
    Prefix      string
    Routes      []APIRoute
    RateLimiter fiber.Handler
}

// registerAPIRoutes sets up the API routing for a given Fiber router.
// It configures API versioning and registers health checks, as well as
// custom application routes.
//
// Parameters:
//
//  api: The Fiber router on which to register the routes.
//  appName: A string representing the name of the application, used for WebSocket subprotocols.
//  monitorPath: The path for the server monitoring route.
//  db: The database service interface used for data-related operations.
func registerAPIRoutes(api fiber.Router, appName, monitorPath string, db database.Service) {
	v1 := api.Group("/v1", func(c *fiber.Ctx) error { // "/v1/" prefix
		c.Set("Version", "v1")
		return c.Next()
	})
	v2 := api.Group("/v2", func(c *fiber.Ctx) error { // "/v2/" prefix
		c.Set("Version", "v2")
		return c.Next()
	})
    // ... function implementation ...
}

// theAPIs registers custom application routes for different API versions.
//
// Parameters:
//
//  v1: Fiber router for version 1 of the API.
//  v2: Fiber router for version 2 of the API.
//  appName: A string representing the name of the application, used for WebSocket subprotocols.
//  monitorPath: The path for the server monitoring route.
//  db: The database service interface used for data-related operations.
//  siteRateLimiter: A rate limiter middleware applied to certain routes.
func theAPIs(v1, v2 fiber.Router, appName, monitorPath string, db database.Service, siteRateLimiter fiber.Handler) {
	// Define the API groups and routes
	apiGroups := []APIGroup{
		{
			Prefix: "/my-project/server",
			RateLimiter: siteRateLimiter,
			Routes: []APIRoute{
				{Path: monitorPath, Method: fiber.MethodGet, Handler: monitor.Server},
			},
		},
	}
	// Register the API routes for each version
	for _, group := range apiGroups {
		registerGroup(v1, group)
	}
    // ... function implementation ...
}

// registerGroup adds all routes from an APIGroup to a specific Fiber router.
//
// Parameters:
//
//  router: The Fiber router on which to register the group's routes.
//  group: The APIGroup containing the routes to be registered.
func registerGroup(router fiber.Router, group APIGroup) {
	g := router.Group(group.Prefix)

	if group.RateLimiter != nil {
		g.Use(group.RateLimiter)
	}

	for _, route := range group.Routes {
		if route.RateLimiter != nil {
			g.Add(route.Method, route.Path, route.RateLimiter, route.Handler)
		} else {
			g.Add(route.Method, route.Path, route.Handler)
		}
	}
    // ... function implementation ...
}

Note

This method currently works only for the Fiber framework, as I haven't tested it with other frameworks.

Disclaimer

  • I agree
@H0llyW00dzZ H0llyW00dzZ added the enhancement New feature or request label Apr 8, 2024
@H0llyW00dzZ
Copy link
Contributor Author

H0llyW00dzZ commented Apr 8, 2024

Note

Additionally, it should be noted that these methods are compatible with the latest version of Go (I'm using) and provide a more straightforward integration with databases.

Example:

  		Prefix: "/gopher",
  		Routes: []APIRoute{
  			{Path: "/reload", Method: fiber.MethodPost, Handler: func(c *fiber.Ctx) error { return users.ReloadConfigHandler(c, db) }},
  		},

then it will be https://go.dev/v1/gopher/reload (example)

@H0llyW00dzZ
Copy link
Contributor Author

Updated:

The helper functions for these methods that can be used for custom error handling independently to avoid conflicts:

// isVersionedAPIRoute checks if the given path matches a versioned API route.
func isVersionedAPIRoute(path string) bool {
    // Define the versioned API route prefixes
    versionedAPIPrefixes := []string{"/v1", "/v2"}

    // Check if the path starts with any of the versioned API prefixes
    for _, prefix := range versionedAPIPrefixes {
        if len(path) >= len(prefix) && path[:len(prefix)] == prefix {
            return true
        }
    }

    return false
}

// apiInternalServerErrorHandler is a custom error handler for internal server errors in versioned APIs.
func apiInternalServerErrorHandler(c *fiber.Ctx, err error) error {
    // Log the error
    Logger.LogErrorf("Internal Server Error occurred in versioned API: %v", err)
    // Return a JSON response with the 500 Internal Server Error status code
    return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
        "error": fiber.ErrInternalServerError.Message,
    })
}

// apiErrorHandler is a custom error handler for versioned APIs.
// It returns JSON responses for 404 (Not Found) and 403 (Forbidden) status codes.
func apiErrorHandler(c *fiber.Ctx) error {
    // Check if the response status code is already set
    if c.Response().StatusCode() != fiber.StatusOK {
        // Get the current status code
        statusCode := c.Response().StatusCode()

        // Check if the status code is 404 or 403
        if statusCode == fiber.StatusNotFound {
            // Return a JSON response for 404 (Not Found) error
            return c.Status(statusCode).JSON(fiber.Map{
                "error": fiber.ErrNotFound.Message,
            })
        } else if statusCode == fiber.StatusForbidden {
            // Return a JSON response for 403 (Forbidden) error
            return c.Status(statusCode).JSON(fiber.Map{
                "error": fiber.ErrForbidden.Message,
            })
        }
    }

    // If the status code is not 404 or 403, continue with the original request
    return c.Next()
}

Then, you can simplify and call them like this:

    // Set custom error handler for the application
    app.Use(func(c *fiber.Ctx) error {
        // Call the next route handler and catch any errors
        err := c.Next()

        // If there was an error, use the custom error handler
        if err != nil {
            // Check if the error is a 404 or 403 error for versioned APIs
            if e, ok := err.(*fiber.Error); ok {
                if e.Code == fiber.StatusNotFound || e.Code == fiber.StatusForbidden {
                    // Check if the request path matches a versioned API route
                    if isVersionedAPIRoute(c.Path()) {
                        // Return a JSON response for 404 or 403 errors in versioned APIs
                        return c.Status(e.Code).JSON(fiber.Map{
                            "error": e.Message,
                        })
                    }
                }
            }

            // Check if the error is a 404 or 403 error for frontend routes
            if e, ok := err.(*fiber.Error); ok {
                if e.Code == fiber.StatusNotFound {
                    // Check if the request path matches a frontend route
                    if isFrontendRoute(c.Path()) {
                        // Render the 404 error page for frontend routes
                        return webPageHandler.PageNotFoundHandler(c)
                    }
                } else if e.Code == fiber.StatusForbidden {
                    // Check if the request path matches a frontend route
                    if isFrontendRoute(c.Path()) {
                        // Render the 403 error page for frontend routes
                        return webPageHandler.PageForbidden403Handler(c)
                    }
                }
            }

            // If the error is not a 404 or 403 error for versioned APIs or frontend routes, check if it's an internal server error
            if e, ok := err.(*fiber.Error); ok && e.Code == fiber.StatusInternalServerError {
                // Return a JSON response for internal server errors in versioned APIs
                return apiInternalServerErrorHandler(c, err)
            }

            // If the error is not a versioned API error, frontend error, or internal server error, fallback to the general error page
            return webPageHandler.Page500InternalServerHandler(c, err)
        }

        // ... rest of the code ...
    })

When a versioned API route is not found, it will use the original message provided by the Fiber framework, as it is reusable.
Example:

image

Note

Also note that Page500InternalServerHandler and apiInternalServerErrorHandler are for handling manipulate panics.

@briancbarrow
Copy link
Collaborator

Like we mentioned in the other issues/PRs, versioning is something that can be handled by each user as they see fit.

@briancbarrow briancbarrow closed this as not planned Won't fix, can't repro, duplicate, stale Apr 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants