mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- Initialize git repository - Add comprehensive .gitignore for Go projects - Install golangci-lint v2.6.0 (latest v2) globally - Configure .golangci.yml with appropriate linters and formatters - Fix all formatting issues (gofmt) - Fix all errcheck issues (unchecked errors) - Adjust complexity threshold for validation functions - All checks passing: build, test, vet, lint
631 lines
14 KiB
Markdown
631 lines
14 KiB
Markdown
# Echo Framework Development Guide
|
|
|
|
**Library**: `github.com/labstack/echo/v4`
|
|
**Used In**: MVP - Alternative HTTP framework to Gin
|
|
**Purpose**: High-performance, extensible web framework
|
|
|
|
---
|
|
|
|
## Where It's Used
|
|
|
|
- **Alternative to Gin** if cleaner API needed
|
|
- HTTP API server
|
|
- Business registration, resource flow CRUD
|
|
- Match retrieval endpoints
|
|
- Authentication middleware
|
|
|
|
---
|
|
|
|
## Official Documentation
|
|
|
|
- **GitHub**: https://github.com/labstack/echo
|
|
- **Official Docs**: https://echo.labstack.com/docs
|
|
- **GoDoc**: https://pkg.go.dev/github.com/labstack/echo/v4
|
|
- **Examples**: https://github.com/labstack/echo/tree/master/examples
|
|
|
|
---
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
go get github.com/labstack/echo/v4
|
|
go get github.com/labstack/echo/v4/middleware
|
|
```
|
|
|
|
---
|
|
|
|
## Key Concepts
|
|
|
|
### 1. Basic Server Setup
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4/middleware"
|
|
)
|
|
|
|
func main() {
|
|
e := echo.New()
|
|
|
|
// Middleware
|
|
e.Use(middleware.Logger())
|
|
e.Use(middleware.Recover())
|
|
|
|
// Routes
|
|
e.GET("/health", healthHandler)
|
|
|
|
// Start server
|
|
e.Logger.Fatal(e.Start(":8080"))
|
|
}
|
|
```
|
|
|
|
### 2. Routes and Handlers
|
|
|
|
```go
|
|
// GET request
|
|
e.GET("/api/businesses/:id", getBusiness)
|
|
|
|
// POST request
|
|
e.POST("/api/businesses", createBusiness)
|
|
|
|
// PUT request
|
|
e.PUT("/api/businesses/:id", updateBusiness)
|
|
|
|
// DELETE request
|
|
e.DELETE("/api/businesses/:id", deleteBusiness)
|
|
|
|
// Handler function
|
|
func getBusiness(c echo.Context) error {
|
|
id := c.Param("id") // Get path parameter
|
|
|
|
business, err := service.GetBusiness(id)
|
|
if err != nil {
|
|
return c.JSON(404, map[string]string{"error": "Not found"})
|
|
}
|
|
|
|
return c.JSON(200, business)
|
|
}
|
|
```
|
|
|
|
### 3. Request Binding
|
|
|
|
```go
|
|
// JSON binding
|
|
type BusinessRequest struct {
|
|
Name string `json:"name" validate:"required"`
|
|
Email string `json:"email" validate:"required,email"`
|
|
}
|
|
|
|
func createBusiness(c echo.Context) error {
|
|
var req BusinessRequest
|
|
|
|
// Bind JSON
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(400, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
// Validate
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(400, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
business, err := service.CreateBusiness(req)
|
|
if err != nil {
|
|
return c.JSON(500, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(201, business)
|
|
}
|
|
|
|
// Query parameters
|
|
func searchBusinesses(c echo.Context) error {
|
|
name := c.QueryParam("name")
|
|
page := c.QueryParamDefault("page", "1")
|
|
// ... process query
|
|
return c.JSON(200, results)
|
|
}
|
|
|
|
// Path parameters
|
|
func getBusiness(c echo.Context) error {
|
|
id := c.Param("id") // /businesses/:id
|
|
// ...
|
|
}
|
|
|
|
// Form data
|
|
func createBusinessForm(c echo.Context) error {
|
|
name := c.FormValue("name")
|
|
email := c.FormValue("email")
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### 4. Middleware
|
|
|
|
```go
|
|
// Global middleware
|
|
e.Use(middleware.Logger())
|
|
e.Use(middleware.Recover())
|
|
e.Use(corsMiddleware())
|
|
|
|
// Route-specific middleware
|
|
e.GET("/protected", protectedHandler, authMiddleware())
|
|
|
|
// Custom middleware
|
|
func authMiddleware() echo.MiddlewareFunc {
|
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
|
return func(c echo.Context) error {
|
|
token := c.Request().Header.Get("Authorization")
|
|
if token == "" {
|
|
return c.JSON(401, map[string]string{"error": "Unauthorized"})
|
|
}
|
|
|
|
// Validate token
|
|
userID, err := validateToken(token)
|
|
if err != nil {
|
|
return c.JSON(401, map[string]string{"error": "Invalid token"})
|
|
}
|
|
|
|
// Store in context
|
|
c.Set("userID", userID)
|
|
|
|
return next(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Built-in middleware
|
|
e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
|
Format: "method=${method}, uri=${uri}, status=${status}\n",
|
|
}))
|
|
```
|
|
|
|
### 5. Response Handling
|
|
|
|
```go
|
|
// JSON response
|
|
return c.JSON(200, business)
|
|
|
|
// JSON with status code
|
|
return c.JSONPretty(200, business, " ") // Pretty print
|
|
|
|
// String response
|
|
return c.String(200, "Success")
|
|
|
|
// HTML response
|
|
return c.HTML(200, "<h1>Hello</h1>")
|
|
|
|
// Redirect
|
|
return c.Redirect(302, "/api/v1/businesses")
|
|
|
|
// Stream
|
|
return c.Stream(200, "application/json", reader)
|
|
|
|
// File download
|
|
return c.Attachment("file.pdf", "document.pdf")
|
|
```
|
|
|
|
### 6. Error Handling
|
|
|
|
```go
|
|
// Custom HTTP error
|
|
func getBusiness(c echo.Context) error {
|
|
business, err := service.GetBusiness(c.Param("id"))
|
|
if err != nil {
|
|
if err == ErrNotFound {
|
|
return echo.NewHTTPError(404, "Business not found")
|
|
}
|
|
return echo.NewHTTPError(500, "Internal server error")
|
|
}
|
|
return c.JSON(200, business)
|
|
}
|
|
|
|
// Error handler
|
|
func customHTTPErrorHandler(err error, c echo.Context) {
|
|
code := http.StatusInternalServerError
|
|
message := "Internal Server Error"
|
|
|
|
if he, ok := err.(*echo.HTTPError); ok {
|
|
code = he.Code
|
|
message = he.Message.(string)
|
|
}
|
|
|
|
c.JSON(code, map[string]interface{}{
|
|
"error": message,
|
|
"code": code,
|
|
})
|
|
}
|
|
|
|
e.HTTPErrorHandler = customHTTPErrorHandler
|
|
```
|
|
|
|
### 7. Grouping Routes
|
|
|
|
```go
|
|
// Route groups
|
|
api := e.Group("/api")
|
|
{
|
|
api.GET("/businesses", listBusinesses)
|
|
api.POST("/businesses", createBusiness)
|
|
|
|
// Nested groups
|
|
v1 := api.Group("/v1")
|
|
{
|
|
v1.GET("/businesses", listBusinessesV1)
|
|
}
|
|
|
|
v2 := api.Group("/v2")
|
|
{
|
|
v2.GET("/businesses", listBusinessesV2)
|
|
}
|
|
}
|
|
|
|
// With middleware
|
|
authenticated := e.Group("/api")
|
|
authenticated.Use(authMiddleware())
|
|
{
|
|
authenticated.GET("/profile", getProfile)
|
|
authenticated.POST("/resources", createResource)
|
|
}
|
|
```
|
|
|
|
### 8. Context Usage
|
|
|
|
```go
|
|
// Get values from context (set by middleware)
|
|
userID := c.Get("userID").(string)
|
|
|
|
// Set value in context
|
|
c.Set("businessID", businessID)
|
|
|
|
// Get request context
|
|
ctx := c.Request().Context()
|
|
|
|
// Create request with context
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
req := c.Request().WithContext(ctx)
|
|
c.SetRequest(req)
|
|
```
|
|
|
|
### 9. File Upload
|
|
|
|
```go
|
|
func uploadFile(c echo.Context) error {
|
|
// Single file
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
return echo.NewHTTPError(400, err.Error())
|
|
}
|
|
|
|
// Open file
|
|
src, err := file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer src.Close()
|
|
|
|
// Save file
|
|
dst, err := os.Create("/uploads/" + file.Filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer dst.Close()
|
|
|
|
if _, err = io.Copy(dst, src); err != nil {
|
|
return err
|
|
}
|
|
|
|
return c.JSON(200, map[string]string{"message": "File uploaded"})
|
|
}
|
|
```
|
|
|
|
### 10. Validator Integration
|
|
|
|
```go
|
|
import "github.com/go-playground/validator/v10"
|
|
|
|
// Custom validator
|
|
type CustomValidator struct {
|
|
validator *validator.Validate
|
|
}
|
|
|
|
func (cv *CustomValidator) Validate(i interface{}) error {
|
|
return cv.validator.Struct(i)
|
|
}
|
|
|
|
// Setup
|
|
e.Validator = &CustomValidator{validator: validator.New()}
|
|
|
|
// Use in handler
|
|
type BusinessRequest struct {
|
|
Name string `validate:"required,min=3"`
|
|
Email string `validate:"required,email"`
|
|
}
|
|
|
|
func createBusiness(c echo.Context) error {
|
|
var req BusinessRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.Validate(&req); err != nil {
|
|
return echo.NewHTTPError(400, err.Error())
|
|
}
|
|
|
|
// ... process request
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## MVP-Specific Patterns
|
|
|
|
### Resource Flow Handler Example
|
|
|
|
```go
|
|
type ResourceFlowHandler struct {
|
|
service *ResourceFlowService
|
|
}
|
|
|
|
func (h *ResourceFlowHandler) Create(c echo.Context) error {
|
|
var req CreateResourceFlowRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return echo.NewHTTPError(400, err.Error())
|
|
}
|
|
|
|
// Validate business belongs to user
|
|
userID := c.Get("userID").(string)
|
|
if !h.service.ValidateOwnership(req.BusinessID, userID) {
|
|
return echo.NewHTTPError(403, "Forbidden")
|
|
}
|
|
|
|
flow, err := h.service.Create(c.Request().Context(), req)
|
|
if err != nil {
|
|
return echo.NewHTTPError(500, err.Error())
|
|
}
|
|
|
|
return c.JSON(201, flow)
|
|
}
|
|
|
|
func (h *ResourceFlowHandler) FindMatches(c echo.Context) error {
|
|
flowID := c.Param("id")
|
|
|
|
matches, err := h.service.FindMatches(c.Request().Context(), flowID)
|
|
if err != nil {
|
|
return echo.NewHTTPError(500, err.Error())
|
|
}
|
|
|
|
return c.JSON(200, map[string]interface{}{
|
|
"matches": matches,
|
|
})
|
|
}
|
|
|
|
// Register routes
|
|
func setupRoutes(e *echo.Echo, handlers *Handlers) {
|
|
api := e.Group("/api/v1")
|
|
|
|
resources := api.Group("/resource-flows")
|
|
resources.Use(authMiddleware())
|
|
{
|
|
resources.POST("", handlers.ResourceFlow.Create)
|
|
resources.GET("/:id/matches", handlers.ResourceFlow.FindMatches)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Middleware for Our MVP
|
|
|
|
```go
|
|
// CORS middleware
|
|
func corsMiddleware() echo.MiddlewareFunc {
|
|
return middleware.CORSWithConfig(middleware.CORSConfig{
|
|
AllowOrigins: []string{"*"},
|
|
AllowMethods: []string{echo.GET, echo.POST, echo.PUT, echo.DELETE, echo.OPTIONS},
|
|
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAuthorization},
|
|
})
|
|
}
|
|
|
|
// Rate limiting
|
|
func rateLimitMiddleware() echo.MiddlewareFunc {
|
|
// Use token bucket or sliding window
|
|
store := memory.NewStore()
|
|
limiter := middleware.NewRateLimiterMemoryStore(store, 100, time.Minute)
|
|
|
|
return middleware.RateLimiter(limiter)
|
|
}
|
|
|
|
// Request ID
|
|
e.Use(middleware.RequestID())
|
|
|
|
// Body limit
|
|
e.Use(middleware.BodyLimit("2M"))
|
|
```
|
|
|
|
---
|
|
|
|
## Performance Tips
|
|
|
|
1. **Use middleware selectively** - don't use unnecessary middleware
|
|
2. **Enable gzip** - use `middleware.Gzip()`
|
|
3. **Use context for cancellation** - `c.Request().Context()` for timeouts
|
|
4. **Cache static files** - use `middleware.Static()`
|
|
5. **Reuse echo instance** - don't create new instance per request
|
|
|
|
---
|
|
|
|
## Built-in Middleware
|
|
|
|
```go
|
|
// Logger
|
|
e.Use(middleware.Logger())
|
|
|
|
// Recover (panic recovery)
|
|
e.Use(middleware.Recover())
|
|
|
|
// CORS
|
|
e.Use(middleware.CORS())
|
|
|
|
// Gzip compression
|
|
e.Use(middleware.Gzip())
|
|
|
|
// Request ID
|
|
e.Use(middleware.RequestID())
|
|
|
|
// Body limit
|
|
e.Use(middleware.BodyLimit("2M"))
|
|
|
|
// Timeout
|
|
e.Use(middleware.TimeoutWithConfig(middleware.TimeoutConfig{
|
|
Timeout: 10 * time.Second,
|
|
}))
|
|
|
|
// Rate limiting
|
|
e.Use(middleware.RateLimiter(limiter))
|
|
|
|
// Static files
|
|
e.Use(middleware.Static("/static"))
|
|
|
|
// Basic auth
|
|
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
|
|
// Validate credentials
|
|
return username == "admin" && password == "secret", nil
|
|
}))
|
|
```
|
|
|
|
---
|
|
|
|
## WebSocket Support
|
|
|
|
```go
|
|
import "github.com/labstack/echo/v4"
|
|
|
|
// WebSocket route
|
|
e.GET("/ws", func(c echo.Context) error {
|
|
return c.Echo().Upgrade(c.Response().Writer, c.Request())
|
|
})
|
|
|
|
// WebSocket handler
|
|
e.GET("/ws", func(c echo.Context) error {
|
|
ws, err := c.WebSocket()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer ws.Close()
|
|
|
|
for {
|
|
// Read message
|
|
msg := new(Message)
|
|
if err := ws.ReadJSON(msg); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process message
|
|
response := processMessage(msg)
|
|
|
|
// Send response
|
|
if err := ws.WriteJSON(response); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Graceful Shutdown
|
|
|
|
```go
|
|
func main() {
|
|
e := echo.New()
|
|
// ... setup routes ...
|
|
|
|
// Start server in goroutine
|
|
go func() {
|
|
if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
|
|
e.Logger.Fatal("shutting down the server")
|
|
}
|
|
}()
|
|
|
|
// Wait for interrupt signal
|
|
quit := make(chan os.Signal, 1)
|
|
signal.Notify(quit, os.Interrupt)
|
|
<-quit
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
if err := e.Shutdown(ctx); err != nil {
|
|
e.Logger.Fatal(err)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
```go
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
func TestGetBusiness(t *testing.T) {
|
|
e := echo.New()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/businesses/123", nil)
|
|
rec := httptest.NewRecorder()
|
|
c := e.NewContext(req, rec)
|
|
|
|
// Setup handler
|
|
handler := &BusinessHandler{service: mockService}
|
|
|
|
// Execute
|
|
if assert.NoError(t, handler.Get(c)) {
|
|
assert.Equal(t, http.StatusOK, rec.Code)
|
|
assert.Contains(t, rec.Body.String(), "business")
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Tutorials & Resources
|
|
|
|
- **Official Examples**: https://github.com/labstack/echo/tree/master/examples
|
|
- **Echo Cookbook**: https://echo.labstack.com/cookbook/
|
|
- **Middleware Guide**: https://echo.labstack.com/middleware/
|
|
- **Binding Guide**: https://echo.labstack.com/guide/binding/
|
|
- **Validation**: https://echo.labstack.com/guide/request/
|
|
|
|
---
|
|
|
|
## Best Practices
|
|
|
|
1. **Use dependency injection** - inject services into handlers
|
|
2. **Handle errors consistently** - use custom error handler
|
|
3. **Validate early** - use validator middleware
|
|
4. **Use middleware for cross-cutting concerns** - auth, logging, CORS
|
|
5. **Version your API** - use route groups
|
|
6. **Context for cancellation** - use `c.Request().Context()` for timeouts
|
|
7. **Graceful shutdown** - implement proper shutdown handling
|
|
|
|
---
|
|
|
|
## Comparison: Echo vs Gin
|
|
|
|
| Feature | Echo | Gin |
|
|
|---------|------|-----|
|
|
| API Style | More structured | More flexible |
|
|
| Middleware | Explicit | Functional |
|
|
| Error Handling | HTTP errors | Error handler |
|
|
| Validation | Built-in support | Requires addon |
|
|
| Context | Request context | Custom context |
|
|
| Performance | High | Very High |
|
|
| Learning Curve | Medium | Low |
|
|
|
|
**Recommendation for MVP**: Both work well, choose based on team preference. Echo has better validation integration, Gin is simpler.
|
|
|