diff --git a/backend/internal/controllers/controller.go b/backend/internal/controllers/controller.go
new file mode 100644
index 0000000..9686bec
--- /dev/null
+++ b/backend/internal/controllers/controller.go
@@ -0,0 +1,141 @@
+// Copyright (c) 2025 Nikolai Papin
+//
+// This file is part of Easywish
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+// the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package controllers
+
+import (
+ "easywish/internal/dto"
+ errs "easywish/internal/errors"
+ "easywish/internal/middleware"
+ "easywish/internal/services"
+ "easywish/internal/utils/enums"
+ "easywish/internal/validation"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-playground/validator/v10"
+ "go.uber.org/zap"
+)
+
+var (
+ GET = "GET"
+ POST = "POST"
+ PUT = "PUT"
+ PATCH = "PATCH"
+ DELETE = "DELETE"
+)
+
+type ControllerMethod struct {
+ HttpMethod string
+ Path string
+ Authorization enums.Role
+ Middleware []gin.HandlerFunc
+ Function func (c *gin.Context)
+}
+
+type controllerImpl struct {
+ Path string
+ Authorization enums.Role
+ Middleware []gin.HandlerFunc
+ Methods []ControllerMethod
+}
+
+func (ctrl *controllerImpl) Setup(group *gin.RouterGroup, log *zap.Logger, auth services.AuthService) *gin.RouterGroup {
+ ctrlGroup := group.Group(ctrl.Path)
+ ctrlGroup.Use(middleware.AuthMiddleware(log, auth))
+ ctrlGroup.Use(gin.HandlerFunc(func(c *gin.Context) {
+ ip := c.ClientIP()
+ userAgent := c.Request.UserAgent()
+ sessionInfoFromCtx, ok := c.Get("session_info"); if !ok {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid or missing session data"})
+ return
+ }
+
+ sessionInfo := sessionInfoFromCtx.(dto.SessionInfo)
+
+ if sessionInfo.Role < ctrl.Authorization {
+ c.AbortWithStatusJSON(
+ http.StatusForbidden,
+ gin.H{"error": "Insufficient authorization for this controller"})
+ return
+ }
+
+ c.Set("client_info", dto.ClientInfo{
+ SessionInfo: sessionInfo,
+ IP: ip,
+ UserAgent: userAgent,
+ })
+
+ c.Next()
+ }))
+ ctrlGroup.Use(ctrl.Middleware...)
+
+ for _, method := range ctrl.Methods {
+ ctrlGroup.Handle(
+ method.HttpMethod,
+ method.Path,
+ append(
+ method.Middleware,
+ gin.HandlerFunc(func(c *gin.Context) {
+ clientInfo, _ := c.Get("client_info")
+ if clientInfo.(dto.ClientInfo).Role < method.Authorization {
+ c.AbortWithStatusJSON(
+ http.StatusForbidden,
+ gin.H{"error": "Insufficient authorization for this method"})
+ return
+ }
+ }),
+ method.Function)...,
+ )}
+ return ctrlGroup
+}
+
+type Controller interface {
+ Setup()
+}
+
+func GetRequest[ModelT any](c *gin.Context) (*dto.Request[ModelT], error) {
+
+ var body ModelT
+ if err := c.ShouldBindJSON(&body); err != nil {
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return nil, err
+ }
+
+ validate := validation.NewValidator()
+
+ if err := validate.Struct(body); err != nil {
+ errorList := err.(validator.ValidationErrors)
+ c.AbortWithStatusJSON(
+ http.StatusBadRequest,
+ gin.H{"error": errorList})
+ return nil, err
+ }
+
+ cinfoFromCtx, ok := c.Get("client_info"); if !ok {
+ c.AbortWithStatusJSON(
+ http.StatusInternalServerError,
+ gin.H{"error": "Client info was not found"})
+ return nil, errs.ErrClientInfoNotProvided
+ }
+ cinfo := cinfoFromCtx.(*dto.ClientInfo)
+
+ return &dto.Request[ModelT]{
+ Body: body,
+ User: *cinfo,
+ }, nil
+}
diff --git a/backend/internal/errors/controller.go b/backend/internal/errors/controller.go
new file mode 100644
index 0000000..4489316
--- /dev/null
+++ b/backend/internal/errors/controller.go
@@ -0,0 +1,24 @@
+// Copyright (c) 2025 Nikolai Papin
+//
+// This file is part of Easywish
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
+// the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+package errors
+
+import "errors"
+
+var (
+ ErrClientInfoNotProvided = errors.New("No client info provded")
+)