Created
April 2, 2025 09:36
-
-
Save sbusso/df100d0b328e22fb97c300aa97a4dc40 to your computer and use it in GitHub Desktop.
Minimal Go backend with Clerk authentication
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"context" | |
"fmt" | |
"net/http" | |
"os" | |
"os/signal" | |
"syscall" | |
"time" | |
"github.com/clerk/clerk-sdk-go/v2" | |
clerkhttp "github.com/clerk/clerk-sdk-go/v2/http" | |
"github.com/joho/godotenv" | |
"github.com/labstack/echo/v4" | |
"github.com/labstack/echo/v4/middleware" | |
"github.com/labstack/gommon/log" | |
) | |
// ClerkMiddleware creates a middleware for Echo that handles Clerk authentication | |
func ClerkMiddleware(next echo.HandlerFunc) echo.HandlerFunc { | |
return func(c echo.Context) error { | |
// Use the Clerk SDK's middleware function under the hood | |
handler := clerkhttp.WithHeaderAuthorization()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
// Update Echo's context with the modified request context from Clerk | |
c.SetRequest(r) | |
// Continue processing the request | |
next(c) | |
})) | |
// Call the Clerk middleware handler | |
handler.ServeHTTP(c.Response().Writer, c.Request()) | |
// The status code is already written by the Clerk middleware if there was an error | |
// So we only return nil to prevent Echo from trying to write a response again | |
return nil | |
} | |
} | |
func main() { | |
// Load environment variables from .env file | |
if err := godotenv.Load(); err != nil { | |
log.Warn("No .env file found") | |
} | |
// Initialize Clerk with API key from environment variable | |
clerkAPIKey := os.Getenv("CLERK_API_KEY") | |
if clerkAPIKey == "" { | |
log.Fatal("CLERK_API_KEY environment variable is required") | |
} | |
clerk.SetKey(clerkAPIKey) | |
// Create a new Echo instance | |
e := echo.New() | |
e.Logger.SetLevel(log.INFO) | |
// Add middleware | |
e.Use(middleware.Logger()) | |
e.Use(middleware.Recover()) | |
e.Use(middleware.CORS()) | |
// Public routes | |
e.GET("/", func(c echo.Context) error { | |
return c.JSON(http.StatusOK, map[string]string{ | |
"access": "public", | |
}) | |
}) | |
// Protected routes | |
api := e.Group("/api") | |
api.Use(ClerkMiddleware) | |
api.GET("", func(c echo.Context) error { | |
// Get the session claims from the context using the Clerk SDK helper | |
claims, ok := clerk.SessionClaimsFromContext(c.Request().Context()) | |
if !ok { | |
return c.JSON(http.StatusUnauthorized, map[string]string{ | |
"access": "unauthorized", | |
}) | |
} | |
return c.JSON(http.StatusOK, map[string]string{ | |
"user_id": claims.Subject, | |
}) | |
}) | |
api.GET("/user/profile", getUserProfile) | |
e.GET("/health", healthCheck) | |
// Start server | |
serverPort := os.Getenv("SERVER_PORT") | |
if serverPort == "" { | |
serverPort = "3000" | |
} | |
// Start server in a goroutine so it doesn't block | |
go func() { | |
if err := e.Start(fmt.Sprintf(":%s", serverPort)); err != nil && err != http.ErrServerClosed { | |
e.Logger.Fatal("shutting down the server") | |
} | |
}() | |
// Wait for interrupt signal to gracefully shutdown the server | |
quit := make(chan os.Signal, 1) | |
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) | |
<-quit | |
// Graceful shutdown | |
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | |
defer cancel() | |
if err := e.Shutdown(ctx); err != nil { | |
e.Logger.Fatal(err) | |
} | |
} | |
// Health check handler | |
func healthCheck(c echo.Context) error { | |
return c.JSON(http.StatusOK, map[string]string{ | |
"status": "healthy", | |
}) | |
} | |
// Get user profile handler | |
func getUserProfile(c echo.Context) error { | |
claims, ok := clerk.SessionClaimsFromContext(c.Request().Context()) | |
if !ok { | |
return c.JSON(http.StatusUnauthorized, map[string]string{ | |
"access": "unauthorized", | |
}) | |
} | |
// Build profile data | |
profile := map[string]interface{}{ | |
"id": claims.ID, | |
} | |
return c.JSON(http.StatusOK, profile) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment