Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions examples/10-production-boilerplate/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ This is a **reference implementation** of a production-ready REST API showcasing
│ │ ├── entity.go # User entity (RICH MODEL)
│ │ ├── repository.go # Data access interface + impl
│ │ ├── service.go # Business logic / use cases
│ │ ├── api.go # HTTP handlers
│ │ └── validation.go # Request validation
│ │ ├── api.go # HTTP handlers (generic Box[Req, Res])
│ │ └── dto.go # Request/response DTOs (validate tags)
│ │
│ ├── notification/ # Notification bounded context
│ │ ├── entity.go
│ │ ├── service.go
│ │ ├── dto.go # Request/response DTOs
│ │ └── api.go # SSE handler
│ │
│ ├── chat/ # Chat bounded context
Expand Down
7 changes: 4 additions & 3 deletions examples/10-production-boilerplate/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ import (
"example.com/production-boilerplate/internal/shared/auth"
"example.com/production-boilerplate/internal/shared/database"
"example.com/production-boilerplate/internal/user"

"github.com/coregx/fursy"
"github.com/coregx/fursy/middleware"
"github.com/coregx/fursy/plugins/validator"
"github.com/coregx/stream/sse"
"github.com/coregx/stream/websocket"
)
Expand Down Expand Up @@ -80,6 +80,7 @@ func main() {

// Create fursy router
router := fursy.New()
router.SetValidator(validator.New())

// Global middleware
router.Use(middleware.Logger())
Expand All @@ -96,12 +97,12 @@ func main() {

// API documentation endpoint
router.Handle("GET", "/", func(c *fursy.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
return c.JSON(http.StatusOK, map[string]any{
"name": "Production Boilerplate API",
"version": "1.0.0",
"docs": "/api/docs",
"health": "/health",
"endpoints": map[string]interface{}{
"endpoints": map[string]any{
"auth": map[string]string{
"register": "POST /api/auth/register",
"login": "POST /api/auth/login",
Expand Down
13 changes: 11 additions & 2 deletions examples/10-production-boilerplate/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ go 1.27

require (
github.com/coregx/fursy v0.7.0
github.com/coregx/fursy/plugins/validator v0.0.0
github.com/coregx/stream v0.1.4
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
golang.org/x/crypto v0.32.0
golang.org/x/crypto v0.55.0
modernc.org/sqlite v1.40.1
)

require (
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.4 // indirect
github.com/leodido/go-urn v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/time v0.16.0 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
Expand All @@ -27,3 +34,5 @@ require (
replace github.com/coregx/fursy => ../..

replace github.com/coregx/fursy/plugins/database => ../../plugins/database

replace github.com/coregx/fursy/plugins/validator => ../../plugins/validator
42 changes: 32 additions & 10 deletions examples/10-production-boilerplate/go.sum
Original file line number Diff line number Diff line change
@@ -1,34 +1,56 @@
github.com/coregx/stream v0.1.4 h1:MQE88pkvuFBlFkrtogk1X39CZJG8oE+avkx3+Y5GJq4=
github.com/coregx/stream v0.1.4/go.mod h1:Nv8tdDu8yQ8jK7ctayRkXdmk3TMX1sS8En2qg0Izz7A=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.4 h1:9Rcod2ZPO6mOEG6b4GqyoHE/H6//Ze0RuhOo1hT1x0w=
github.com/go-playground/validator/v10 v10.30.4/go.mod h1:numpT+RPLE91R9oYWMY/R9zRgJBewr3IXHko4OISPpk=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
Expand Down
4 changes: 2 additions & 2 deletions examples/10-production-boilerplate/internal/chat/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@ func (api *API) RegisterRoutes(r *fursy.Router, authMiddleware fursy.HandlerFunc
protected := r.Group("/api")
protected.Use(authMiddleware)
{
protected.Handle("GET", "/chat/ws", api.websocket)
protected.GET("/chat/ws", api.websocket)
}
}

// websocket handles WebSocket chat connection.
func (api *API) websocket(c *fursy.Context) error {
func (api *API) websocket(c *fursy.Box[fursy.Empty, fursy.Empty]) error {
// Get user info from context
userID := auth.GetUserID(c.Request.Context())

Expand Down
34 changes: 6 additions & 28 deletions examples/10-production-boilerplate/internal/notification/api.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package notification

import (
"encoding/json"
"net/http"

"github.com/coregx/fursy"
"github.com/coregx/stream/sse"
)
Expand All @@ -27,13 +24,13 @@ func (api *API) RegisterRoutes(r *fursy.Router, authMiddleware fursy.HandlerFunc
protected := r.Group("/api")
protected.Use(authMiddleware)
{
protected.Handle("GET", "/notifications/stream", api.stream)
protected.Handle("POST", "/notifications/broadcast", api.broadcast)
protected.GET("/notifications/stream", api.stream)
protected.POST("/notifications/broadcast", api.broadcast)
}
}

// stream handles SSE notification stream.
func (api *API) stream(c *fursy.Context) error {
func (api *API) stream(c *fursy.Box[fursy.Empty, fursy.Empty]) error {
// Upgrade to SSE connection
conn, err := sse.Upgrade(c.Response, c.Request)
if err != nil {
Expand All @@ -50,29 +47,10 @@ func (api *API) stream(c *fursy.Context) error {
}

// broadcast broadcasts notification to all connected clients (admin only).
func (api *API) broadcast(c *fursy.Context) error {
var req struct {
Message string `json:"message"`
Type NotificationType `json:"type"`
}

if err := json.NewDecoder(c.Request.Body).Decode(&req); err != nil {
return c.Problem(fursy.BadRequest("Invalid request: " + err.Error()))
}

if req.Message == "" {
return c.Problem(fursy.BadRequest("Message is required"))
}

if req.Type == "" {
req.Type = NotificationTypeInfo
}

if err := api.service.Broadcast(req.Message, req.Type); err != nil {
func (api *API) broadcast(c *fursy.Box[BroadcastRequest, MessageResponse]) error {
if err := api.service.Broadcast(c.ReqBody.Message, c.ReqBody.Type); err != nil {
return c.Problem(fursy.InternalServerError("Broadcast failed: " + err.Error()))
}

return c.JSON(http.StatusOK, map[string]string{
"message": "Notification broadcasted successfully",
})
return c.OK(MessageResponse{Message: "Notification broadcasted successfully"})
}
12 changes: 12 additions & 0 deletions examples/10-production-boilerplate/internal/notification/dto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package notification

// BroadcastRequest is the payload for broadcasting a notification.
type BroadcastRequest struct {
Message string `json:"message" validate:"required"`
Type NotificationType `json:"type"`
}

// MessageResponse is a simple message response.
type MessageResponse struct {
Message string `json:"message"`
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ const (

// NewNotification creates a new notification.
func NewNotification(userID, message string, notifType NotificationType) *Notification {
if notifType == "" {
notifType = NotificationTypeInfo // default value
}

return &Notification{
ID: generateID(),
UserID: userID,
Expand Down
Loading
Loading