Repository: evrone/go-clean-template

Issue:
NATS: integration and Request-Reply pattern
https://docs.nats.io/nats-concepts/core-nats/reqreply

Request-Reply is a common pattern in modern distributed systems. A request is sent, and the application either waits on the response with a certain timeout, or receives a response asynchronously.

NATS supports the Request-Reply pattern using its core communication mechanism — publish and subscribe. A request is published on a given subject using a reply subject. Responders listen on that subject and send responses to the reply subject. Reply subjects are called "inbox". These are unique subjects that are dynamically directed back to the requester, regardless of the location of either party.

Hints from issue comments:

Solution patch:
diff --git a/.env.example b/.env.example
index 7c555d4..887c558 100644
--- a/.env.example
+++ b/.env.example
@@ -15,6 +15,9 @@ GRPC_PORT=8081
 RMQ_RPC_SERVER=rpc_server
 RMQ_RPC_CLIENT=rpc_client
 RMQ_URL=amqp://guest:guest@localhost:5672/
+# NATS
+NATS_RPC_SERVER=rpc_server
+NATS_URL=nats://guest:guest@localhost:4222/
 # Metrics
 METRICS_ENABLED=true
 # Swagger
diff --git a/README.md b/README.md
index feed8a3..30e3e07 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ supported by [Evrone](https://evrone.com/?utm_source=github&utm_campaign=go-clea
 This template implements three types of servers:
 
 - AMQP RPC (based on RabbitMQ as [transport](https://github.com/rabbitmq/amqp091-go))
+- MQ RPC (based on NATS as [transport](https://github.com/nats-io/nats.go))
 - gRPC ([gRPC](https://grpc.io/) framework based on protobuf)
 - REST API ([Fiber](https://github.com/gofiber/fiber) framework)
 
@@ -54,7 +55,7 @@ This template implements three types of servers:
 ### Local development
 
 ```sh
-# Postgres, RabbitMQ
+# Postgres, RabbitMQ, NATS
 make compose-up
 # Run app with migrations
 make run
@@ -79,6 +80,9 @@ Check services:
   - URL: `amqp://guest:guest@127.0.0.1:5672/`
   - Client Exchange: `rpc_client`
   - Server Exchange: `rpc_server`
+- NATS RPC:
+  - URL: `nats://guest:guest@127.0.0.1:4222/`
+  - Server Exchange: `rpc_server`
 - REST API:
   - http://app.lvh.me/healthz | http://127.0.0.1:8080/healthz
   - http://app.lvh.me/metrics | http://127.0.0.1:8080/metrics
@@ -91,6 +95,9 @@ Check services:
 - RabbitMQ:
   - http://rabbitmq.lvh.me | http://127.0.0.1:15672
   - Credentials: `guest` / `guest`
+- NATS monitoring:
+  - http://nats.lvh.me | http://127.0.0.1:8222/
+  - Credentials: `guest` / `guest`
 
 ## Project structure
 
diff --git a/README_CN.md b/README_CN.md
index d4c0385..ef8800e 100644
--- a/README_CN.md
+++ b/README_CN.md
@@ -36,6 +36,7 @@ golang服务的整洁架构模板
 此模板实现了三种类型的服务器：
 
 - AMQP RPC（基于 RabbitMQ 作为传输）
+- NATS RPC（基于 NATS 作为传输）
 - gRPC（基于 protobuf 的 [gRPC](https://grpc.io/) 框架）
 - REST API（基于 [Fiber](https://github.com/gofiber/fiber) 框架）
 
@@ -46,30 +47,12 @@ golang服务的整洁架构模板
 - [依赖注入](#依赖注入)
 - [整洁架构](#整洁架构)
 
-## 快速开始
-
-本地开发
-
-```sh
-# Postgres, RabbitMQ
-make compose-up
-# Run app with migrations
-make run
-```
-
-集成测试（可以在CI中运行）
-
-```sh
-# DB, app + migrations, integration tests
-make compose-up-integration-test
-```
-
 ## Quick start
 
 ### Local development
 
 ```sh
-# Postgres, RabbitMQ
+# Postgres, RabbitMQ, NATS
 make compose-up
 # Run app with migrations
 make run
@@ -94,6 +77,9 @@ Check services:
   - URL: `amqp://guest:guest@127.0.0.1:5672/`
   - Client Exchange: `rpc_client`
   - Server Exchange: `rpc_server`
+- NATS RPC:
+  - URL: `nats://guest:guest@127.0.0.1:4222/`
+  - Server Exchange: `rpc_server`
 - REST API:
   - http://app.lvh.me/healthz | http://127.0.0.1:8080/healthz
   - http://app.lvh.me/metrics | http://127.0.0.1:8080/metrics
@@ -106,6 +92,9 @@ Check services:
 - RabbitMQ:
   - http://rabbitmq.lvh.me | http://127.0.0.1:15672
   - Credentials: `guest` / `guest`
+- NATS monitoring:
+  - http://nats.lvh.me | http://127.0.0.1:8222/
+  - Credentials: `guest` / `guest`
 
 ## 工程架构
 
diff --git a/README_RU.md b/README_RU.md
index 7f45b06..f0f6de1 100644
--- a/README_RU.md
+++ b/README_RU.md
@@ -34,6 +34,7 @@
 Этот шаблон поддерживает три типа серверов:
 
 - AMQP RPC (на основе RabbitMQ в качестве [транспорта](https://github.com/rabbitmq/amqp091-go))
+- NATS RPC (на основе NATS в качестве [транспорта](https://github.com/nats-io/nats.go))
 - gRPC ([gRPC](https://grpc.io/) фреймворк на основе protobuf)
 - REST API ([Fiber](https://github.com/gofiber/fiber) фреймворк)
 
@@ -49,7 +50,7 @@
 ### Локальная разработка
 
 ```sh
-# Postgres, RabbitMQ
+# Postgres, RabbitMQ, NATS
 make compose-up
 # Запуск приложения и миграций
 make run
@@ -74,6 +75,9 @@ make compose-up-all
   - URL: `amqp://guest:guest@127.0.0.1:5672/`
   - Client Exchange: `rpc_client`
   - Server Exchange: `rpc_server`
+- NATS RPC:
+  - URL: `nats://guest:guest@127.0.0.1:4222/`
+  - Server Exchange: `rpc_server`
 - REST API:
   - http://app.lvh.me/healthz | http://127.0.0.1:8080/healthz
   - http://app.lvh.me/metrics | http://127.0.0.1:8080/metrics
@@ -86,6 +90,9 @@ make compose-up-all
 - RabbitMQ:
   - http://rabbitmq.lvh.me | http://127.0.0.1:15672
   - Credentials: `guest` / `guest`
+- NATS monitoring:
+  - http://nats.lvh.me | http://127.0.0.1:8222/
+  - Credentials: `guest` / `guest`
 
 ## Структура проекта
 
diff --git a/config/config.go b/config/config.go
index 35912f6..0e0e001 100644
--- a/config/config.go
+++ b/config/config.go
@@ -15,6 +15,7 @@ type (
 		PG      PG
 		GRPC    GRPC
 		RMQ     RMQ
+		NATS    NATS
 		Metrics Metrics
 		Swagger Swagger
 	}
@@ -54,6 +55,12 @@ type (
 		URL            string `env:"RMQ_URL,required"`
 	}
 
+	// NATS -.
+	NATS struct {
+		ServerExchange string `env:"NATS_RPC_SERVER,required"`
+		URL            string `env:"NATS_URL,required"`
+	}
+
 	// Metrics -.
 	Metrics struct {
 		Enabled bool `env:"METRICS_ENABLED" envDefault:"true"`
diff --git a/docker-compose.yml b/docker-compose.yml
index f4a54a3..587b759 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -32,6 +32,9 @@ x-backend-app-environment: &x-backend-app-environment
   RMQ_RPC_SERVER: "rpc_server"
   RMQ_RPC_CLIENT: "rpc_client"
   RMQ_URL: "amqp://guest:guest@rabbitmq:5672/"
+  # NATS
+  NATS_RPC_SERVER: "rpc_server"
+  NATS_URL: "nats://guest:guest@nats:4222/"
   # Metrics
   METRICS_ENABLED: "true"
   # Swagger
@@ -68,6 +71,20 @@ services:
         aliases:
           - rabbitmq.lvh.me
 
+  nats:
+    container_name: nats
+    image: nats:2.11-alpine
+    command: [ "-m", "8222", "-sd", "/nats-data", "-user", "guest", "-pass", "guest" ]
+    ports:
+      - "4222:4222"
+      - "8222:8222"
+    volumes:
+      - nats_data:/nats-data
+    networks:
+      app_network:
+        aliases:
+          - nats.lvh.me
+
   app:
     container_name: app
     platform: linux/amd64
@@ -110,3 +127,4 @@ networks:
 volumes:
   db_data:
   rabbitmq_data:
+  nats_data:
diff --git a/go.mod b/go.mod
index efb3ad8..ea6f964 100644
--- a/go.mod
+++ b/go.mod
@@ -26,6 +26,7 @@ require (
 	github.com/golang-migrate/migrate/v4 v4.19.0
 	github.com/google/uuid v1.6.0
 	github.com/jackc/pgx/v5 v5.7.6
+	github.com/nats-io/nats.go v1.46.1
 	github.com/rabbitmq/amqp091-go v1.10.0
 	github.com/rs/zerolog v1.34.0
 	github.com/stretchr/testify v1.11.1
@@ -289,6 +290,8 @@ require (
 	github.com/mutecomm/go-sqlcipher/v4 v4.4.0 // indirect
 	github.com/nakabonne/nestif v0.3.1 // indirect
 	github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 // indirect
+	github.com/nats-io/nkeys v0.4.11 // indirect
+	github.com/nats-io/nuid v1.0.1 // indirect
 	github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba // indirect
 	github.com/nishanths/exhaustive v0.12.0 // indirect
 	github.com/nishanths/predeclared v0.2.2 // indirect
diff --git a/go.sum b/go.sum
index 91c2650..2af510d 100644
--- a/go.sum
+++ b/go.sum
@@ -1550,6 +1550,12 @@ github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81
 github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE=
 github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8 h1:P48LjvUQpTReR3TQRbxSeSBsMXzfK0uol7eRcr7VBYQ=
 github.com/nakagami/firebirdsql v0.0.0-20190310045651-3c02a58cfed8/go.mod h1:86wM1zFnC6/uDBfZGNwB65O+pR2OFi5q/YQaEUid1qA=
+github.com/nats-io/nats.go v1.46.1 h1:bqQ2ZcxVd2lpYI97xYASeRTY3I5boe/IVmuUDPitHfo=
+github.com/nats-io/nats.go v1.46.1/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
+github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
+github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
+github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
 github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba h1:fhFP5RliM2HW/8XdcO5QngSfFli9GcRIpMXvypTQt6E=
 github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
diff --git a/internal/app/app.go b/internal/app/app.go
index 670165c..b3afc56 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -11,18 +11,20 @@ import (
 	amqprpc "github.com/evrone/go-clean-template/internal/controller/amqp_rpc"
 	"github.com/evrone/go-clean-template/internal/controller/grpc"
 	"github.com/evrone/go-clean-template/internal/controller/http"
+	natsrpc "github.com/evrone/go-clean-template/internal/controller/nats_rpc"
 	"github.com/evrone/go-clean-template/internal/repo/persistent"
 	"github.com/evrone/go-clean-template/internal/repo/webapi"
 	"github.com/evrone/go-clean-template/internal/usecase/translation"
 	"github.com/evrone/go-clean-template/pkg/grpcserver"
 	"github.com/evrone/go-clean-template/pkg/httpserver"
 	"github.com/evrone/go-clean-template/pkg/logger"
+	natsRPCServer "github.com/evrone/go-clean-template/pkg/nats/nats_rpc/server"
 	"github.com/evrone/go-clean-template/pkg/postgres"
-	"github.com/evrone/go-clean-template/pkg/rabbitmq/rmq_rpc/server"
+	rmqRPCServer "github.com/evrone/go-clean-template/pkg/rabbitmq/rmq_rpc/server"
 )
 
 // Run creates objects via constructors.
-func Run(cfg *config.Config) {
+func Run(cfg *config.Config) { //nolint: gocyclo,cyclop,funlen,gocritic,nolintlint
 	l := logger.New(cfg.Log.Level)
 
 	// Repository
@@ -41,11 +43,19 @@ func Run(cfg *config.Config) {
 	// RabbitMQ RPC Server
 	rmqRouter := amqprpc.NewRouter(translationUseCase, l)
 
-	rmqServer, err := server.New(cfg.RMQ.URL, cfg.RMQ.ServerExchange, rmqRouter, l)
+	rmqServer, err := rmqRPCServer.New(cfg.RMQ.URL, cfg.RMQ.ServerExchange, rmqRouter, l)
 	if err != nil {
 		l.Fatal(fmt.Errorf("app - Run - rmqServer - server.New: %w", err))
 	}
 
+	// NATS RPC Server
+	natsRouter := natsrpc.NewRouter(translationUseCase, l)
+
+	natsServer, err := natsRPCServer.New(cfg.NATS.URL, cfg.NATS.ServerExchange, natsRouter, l)
+	if err != nil {
+		l.Fatal(fmt.Errorf("app - Run - natsServer - server.New: %w", err))
+	}
+
 	// gRPC Server
 	grpcServer := grpcserver.New(l, grpcserver.Port(cfg.GRPC.Port))
 	grpc.NewRouter(grpcServer.App, translationUseCase, l)
@@ -56,6 +66,7 @@ func Run(cfg *config.Config) {
 
 	// Start servers
 	rmqServer.Start()
+	natsServer.Start()
 	grpcServer.Start()
 	httpServer.Start()
 
@@ -72,6 +83,8 @@ func Run(cfg *config.Config) {
 		l.Error(fmt.Errorf("app - Run - grpcServer.Notify: %w", err))
 	case err = <-rmqServer.Notify():
 		l.Error(fmt.Errorf("app - Run - rmqServer.Notify: %w", err))
+	case err = <-natsServer.Notify():
+		l.Error(fmt.Errorf("app - Run - natsServer.Notify: %w", err))
 	}
 
 	// Shutdown
@@ -89,4 +102,9 @@ func Run(cfg *config.Config) {
 	if err != nil {
 		l.Error(fmt.Errorf("app - Run - rmqServer.Shutdown: %w", err))
 	}
+
+	err = natsServer.Shutdown()
+	if err != nil {
+		l.Error(fmt.Errorf("app - Run - natsServer.Shutdown: %w", err))
+	}
 }
diff --git a/internal/controller/nats_rpc/router.go b/internal/controller/nats_rpc/router.go
new file mode 100644
index 0000000..e82465d
--- /dev/null
+++ b/internal/controller/nats_rpc/router.go
@@ -0,0 +1,19 @@
+package v1
+
+import (
+	v1 "github.com/evrone/go-clean-template/internal/controller/nats_rpc/v1"
+	"github.com/evrone/go-clean-template/internal/usecase"
+	"github.com/evrone/go-clean-template/pkg/logger"
+	"github.com/evrone/go-clean-template/pkg/nats/nats_rpc/server"
+)
+
+// NewRouter -.
+func NewRouter(t usecase.Translation, l logger.Interface) map[string]server.CallHandler {
+	routes := make(map[string]server.CallHandler)
+
+	{
+		v1.NewTranslationRoutes(routes, t, l)
+	}
+
+	return routes
+}
diff --git a/internal/controller/nats_rpc/v1/controller.go b/internal/controller/nats_rpc/v1/controller.go
new file mode 100644
index 0000000..36077b3
--- /dev/null
+++ b/internal/controller/nats_rpc/v1/controller.go
@@ -0,0 +1,14 @@
+package v1
+
+import (
+	"github.com/evrone/go-clean-template/internal/usecase"
+	"github.com/evrone/go-clean-template/pkg/logger"
+	"github.com/go-playground/validator/v10"
+)
+
+// V1 -.
+type V1 struct {
+	t usecase.Translation
+	l logger.Interface
+	v *validator.Validate
+}
diff --git a/internal/controller/nats_rpc/v1/router.go b/internal/controller/nats_rpc/v1/router.go
new file mode 100644
index 0000000..7255747
--- /dev/null
+++ b/internal/controller/nats_rpc/v1/router.go
@@ -0,0 +1,17 @@
+package v1
+
+import (
+	"github.com/evrone/go-clean-template/internal/usecase"
+	"github.com/evrone/go-clean-template/pkg/logger"
+	"github.com/evrone/go-clean-template/pkg/nats/nats_rpc/server"
+	"github.com/go-playground/validator/v10"
+)
+
+// NewTranslationRoutes -.
+func NewTranslationRoutes(routes map[string]server.CallHandler, t usecase.Translation, l logger.Interface) {
+	r := &V1{t: t, l: l, v: validator.New(validator.WithRequiredStructEnabled())}
+
+	{
+		routes["v1.getHistory"] = r.getHistory()
+	}
+}
diff --git a/internal/controller/nats_rpc/v1/translation.go b/internal/controller/nats_rpc/v1/translation.go
new file mode 100644
index 0000000..ef6aacf
--- /dev/null
+++ b/internal/controller/nats_rpc/v1/translation.go
@@ -0,0 +1,22 @@
+package v1
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/evrone/go-clean-template/pkg/nats/nats_rpc/server"
+	"github.com/nats-io/nats.go"
+)
+
+func (r *V1) getHistory() server.CallHandler {
+	return func(_ *nats.Msg) (interface{}, error) {
+		translationHistory, err := r.t.History(context.Background())
+		if err != nil {
+			r.l.Error(err, "amqp_rpc - V1 - getHistory")
+
+			return nil, fmt.Errorf("amqp_rpc - V1 - getHistory: %w", err)
+		}
+
+		return translationHistory, nil
+	}
+}
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index 9b0ecd0..bcb1dc2 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -21,6 +21,11 @@ http
         server rabbitmq:15672;
     }
 
+    upstream nats
+    {
+        server nats:8222;
+    }
+
     server
     {
         listen 80;
@@ -68,6 +73,22 @@ http
             proxy_set_header Connection $connection_upgrade;
         }
     }
+
+    server
+    {
+        listen 80;
+        server_name nats.lvh.me;
+
+        location /
+        {
+            proxy_pass http://nats;
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_set_header Upgrade $http_upgrade;
+            proxy_set_header Connection $connection_upgrade;
+        }
+    }
 }
 
 
diff --git a/pkg/nats/nats_rpc/client/client.go b/pkg/nats/nats_rpc/client/client.go
new file mode 100644
index 0000000..236e608
--- /dev/null
+++ b/pkg/nats/nats_rpc/client/client.go
@@ -0,0 +1,111 @@
+package client
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"time"
+
+	natsrpc "github.com/evrone/go-clean-template/pkg/nats/nats_rpc"
+	"github.com/goccy/go-json"
+	"github.com/nats-io/nats.go"
+)
+
+const (
+	_defaultWaitTime = 5 * time.Second
+	_defaultAttempts = 10
+	_defaultTimeout  = 2 * time.Second
+)
+
+// Client -.
+type Client struct {
+	subject    string
+	connection *nats.Conn
+
+	timeout time.Duration
+}
+
+// New -.
+func New(
+	url string,
+	serverSubject string,
+	opts ...Option,
+) (*Client, error) {
+	connection, err := nats.Connect(
+		url,
+		nats.ReconnectWait(_defaultWaitTime),
+		nats.MaxReconnects(_defaultAttempts),
+		nats.Timeout(_defaultWaitTime),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("nats_rpc client - NewClient - nats.Connect: %w", err)
+	}
+
+	c := &Client{
+		subject:    serverSubject,
+		connection: connection,
+		timeout:    _defaultTimeout,
+	}
+
+	// Custom options
+	for _, opt := range opts {
+		opt(c)
+	}
+
+	c.connection = connection
+
+	return c, nil
+}
+
+// Shutdown -.
+func (c *Client) Shutdown() error {
+	c.connection.Close()
+
+	return nil
+}
+
+// RemoteCall -.
+func (c *Client) RemoteCall(handler string, request, response interface{}) error {
+	var (
+		requestBody []byte
+		err         error
+	)
+
+	if request != nil {
+		requestBody, err = json.Marshal(request)
+		if err != nil {
+			return err
+		}
+	}
+
+	requestMessage := nats.Msg{
+		Subject: c.subject,
+		Header: nats.Header{
+			"Handler": []string{handler},
+		},
+		Data: requestBody,
+	}
+
+	message, err := c.connection.RequestMsg(&requestMessage, c.timeout)
+	if errors.Is(err, context.DeadlineExceeded) {
+		return natsrpc.ErrTimeout
+	}
+
+	if err != nil {
+		return fmt.Errorf("nats_rpc client - Client - RemoteCall - c.connection.Conn.Request: %w", err)
+	}
+
+	switch message.Header.Get("Status") {
+	case natsrpc.Success:
+		err = json.Unmarshal(message.Data, &response)
+		if err != nil {
+			return fmt.Errorf("nats_rpc client - Client - RemoteCall - json.Unmarshal: %w", err)
+		}
+	case natsrpc.ErrBadHandler.Error():
+		return natsrpc.ErrBadHandler
+	case natsrpc.ErrInternalServer.Error():
+		return natsrpc.ErrInternalServer
+	}
+
+	return nil
+}
diff --git a/pkg/nats/nats_rpc/client/options.go b/pkg/nats/nats_rpc/client/options.go
new file mode 100644
index 0000000..7f99417
--- /dev/null
+++ b/pkg/nats/nats_rpc/client/options.go
@@ -0,0 +1,13 @@
+package client
+
+import "time"
+
+// Option -.
+type Option func(*Client)
+
+// Timeout -.
+func Timeout(timeout time.Duration) Option {
+	return func(c *Client) {
+		c.timeout = timeout
+	}
+}
diff --git a/pkg/nats/nats_rpc/errors.go b/pkg/nats/nats_rpc/errors.go
new file mode 100644
index 0000000..fe3a9dc
--- /dev/null
+++ b/pkg/nats/nats_rpc/errors.go
@@ -0,0 +1,15 @@
+package natsrpc
+
+import "errors"
+
+var (
+	// ErrTimeout -.
+	ErrTimeout = errors.New("timeout")
+	// ErrInternalServer -.
+	ErrInternalServer = errors.New("internal server error")
+	// ErrBadHandler -.
+	ErrBadHandler = errors.New("unregistered handler")
+)
+
+// Success -.
+const Success = "success"
diff --git a/pkg/nats/nats_rpc/server/options.go b/pkg/nats/nats_rpc/server/options.go
new file mode 100644
index 0000000..bdd4f44
--- /dev/null
+++ b/pkg/nats/nats_rpc/server/options.go
@@ -0,0 +1,13 @@
+package server
+
+import "time"
+
+// Option -.
+type Option func(*Server)
+
+// Timeout -.
+func Timeout(timeout time.Duration) Option {
+	return func(s *Server) {
+		s.timeout = timeout
+	}
+}
diff --git a/pkg/nats/nats_rpc/server/server.go b/pkg/nats/nats_rpc/server/server.go
new file mode 100644
index 0000000..b681fb2
--- /dev/null
+++ b/pkg/nats/nats_rpc/server/server.go
@@ -0,0 +1,193 @@
+// Package server implements NATS RPC server.
+package server
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"time"
+
+	"github.com/evrone/go-clean-template/pkg/logger"
+	natsrpc "github.com/evrone/go-clean-template/pkg/nats/nats_rpc"
+	"github.com/goccy/go-json"
+	"github.com/nats-io/nats.go"
+	"golang.org/x/sync/errgroup"
+)
+
+const (
+	_defaultWaitTime = 5 * time.Second
+	_defaultAttempts = 10
+	_defaultTimeout  = 2 * time.Second
+)
+
+// CallHandler -.
+type CallHandler func(*nats.Msg) (interface{}, error)
+
+// Server -.
+type Server struct {
+	ctx context.Context
+	eg  *errgroup.Group
+
+	subject      string
+	connection   *nats.Conn
+	subscription *nats.Subscription
+	router       map[string]CallHandler
+	stop         chan struct{}
+	notify       chan error
+
+	timeout time.Duration
+
+	logger logger.Interface
+}
+
+// New -.
+func New(
+	url,
+	serverSubject string,
+	router map[string]CallHandler,
+	l logger.Interface,
+	opts ...Option,
+) (*Server, error) {
+	group, ctx := errgroup.WithContext(context.Background())
+	group.SetLimit(1) // Run only one goroutine
+
+	connection, err := nats.Connect(
+		url,
+		nats.ReconnectWait(_defaultWaitTime),
+		nats.MaxReconnects(_defaultAttempts),
+		nats.Timeout(_defaultWaitTime),
+	)
+	if err != nil {
+		return nil, fmt.Errorf("nats_rpc server - NewServer - nats.Connect: %w", err)
+	}
+
+	s := &Server{
+		ctx:        ctx,
+		eg:         group,
+		subject:    serverSubject,
+		connection: connection,
+		router:     router,
+		stop:       make(chan struct{}),
+		notify:     make(chan error, 1),
+		timeout:    _defaultTimeout,
+		logger:     l,
+	}
+
+	// Custom options
+	for _, opt := range opts {
+		opt(s)
+	}
+
+	return s, nil
+}
+
+// Start -.
+func (s *Server) Start() {
+	s.eg.Go(func() error {
+		err := s.subscribe()
+		if err != nil {
+			s.notify <- err
+
+			close(s.notify)
+
+			return err
+		}
+
+		// Wait for stop signal
+		<-s.stop
+
+		return nil
+	})
+
+	s.logger.Info("nats_rpc server - Server - Started")
+}
+
+// Notify -.
+func (s *Server) Notify() <-chan error {
+	return s.notify
+}
+
+// Shutdown -.
+func (s *Server) Shutdown() error {
+	var shutdownErrors []error
+
+	close(s.stop)
+
+	// Wait for all goroutines to finish and get any error
+	err := s.eg.Wait()
+	if err != nil && !errors.Is(err, context.Canceled) {
+		s.logger.Error(err, "nats_rpc server - Server - Shutdown - s.eg.Wait")
+
+		shutdownErrors = append(shutdownErrors, err)
+	}
+
+	// Unsubscribe
+	if s.subscription != nil {
+		err := s.subscription.Unsubscribe()
+		if err != nil {
+			s.logger.Error(err, "nats_rpc server - Server - Shutdown - s.conn.Subscription.Unsubscribe")
+
+			shutdownErrors = append(shutdownErrors, err)
+		}
+	}
+
+	// Close connection
+	s.connection.Close()
+
+	s.logger.Info("nats_rpc server - Server - Shutdown")
+
+	return errors.Join(shutdownErrors...)
+}
+
+func (s *Server) subscribe() error {
+	subscription, err := s.connection.Subscribe(s.subject, s.handleMessage)
+	if err != nil {
+		return fmt.Errorf("nats_rpc server - subscribe - s.conn.AttemptConnect: %w", err)
+	}
+
+	s.subscription = subscription
+
+	return nil
+}
+
+func (s *Server) handleMessage(msg *nats.Msg) {
+	handler := msg.Header.Get("Handler")
+
+	callHandler, ok := s.router[handler]
+	if !ok {
+		s.publish(msg, nil, natsrpc.ErrBadHandler.Error())
+
+		return
+	}
+
+	response, err := callHandler(msg)
+	if err != nil {
+		s.publish(msg, nil, natsrpc.ErrInternalServer.Error())
+
+		s.logger.Error(err, "nats_rpc server - Server - handleMessage - callHandler")
+
+		return
+	}
+
+	body, err := json.Marshal(response)
+	if err != nil {
+		s.logger.Error(err, "nats_rpc server - Server - handleMessage - json.Marshal")
+
+		s.publish(msg, nil, natsrpc.ErrInternalServer.Error())
+
+		return
+	}
+
+	s.publish(msg, body, natsrpc.Success)
+}
+
+func (s *Server) publish(msg *nats.Msg, body []byte, status string) {
+	respondMsg := nats.NewMsg(msg.Reply)
+	respondMsg.Header.Set("Status", status)
+	respondMsg.Data = body
+
+	err := s.connection.PublishMsg(respondMsg)
+	if err != nil {
+		s.logger.Error(err, "nats_rpc server - Server - publish - msg.Respond")
+	}
+}
diff --git a/pkg/rabbitmq/rmq_rpc/client/client.go b/pkg/rabbitmq/rmq_rpc/client/client.go
index 2b52e37..afc6699 100644
--- a/pkg/rabbitmq/rmq_rpc/client/client.go
+++ b/pkg/rabbitmq/rmq_rpc/client/client.go
@@ -138,20 +138,17 @@ func (c *Client) RemoteCall(handler string, request, response interface{}) error
 		return fmt.Errorf("rmq_rpc client - Client - RemoteCall - c.remoteCallWait: %w", err)
 	}
 
-	if call.status == rmqrpc.Success {
+	switch call.status {
+	case rmqrpc.Success:
 		err = json.Unmarshal(call.body, &response)
 		if err != nil {
 			return fmt.Errorf("rmq_rpc client - Client - RemoteCall - json.Unmarshal: %w", err)
 		}
 
 		return nil
-	}
-
-	if call.status == rmqrpc.ErrBadHandler.Error() {
+	case rmqrpc.ErrBadHandler.Error():
 		return rmqrpc.ErrBadHandler
-	}
-
-	if call.status == rmqrpc.ErrInternalServer.Error() {
+	case rmqrpc.ErrInternalServer.Error():
 		return rmqrpc.ErrInternalServer
 	}
 



Test patch:
diff --git a/integration-test/integration_test.go b/integration-test/integration_test.go
index 1e7d9f1..d0d769f 100644
--- a/integration-test/integration_test.go
+++ b/integration-test/integration_test.go
@@ -12,7 +12,8 @@ import (
 	"time"
 
 	protov1 "github.com/evrone/go-clean-template/docs/proto/v1"
-	"github.com/evrone/go-clean-template/pkg/rabbitmq/rmq_rpc/client"
+	natsClient "github.com/evrone/go-clean-template/pkg/nats/nats_rpc/client"
+	rmqClient "github.com/evrone/go-clean-template/pkg/rabbitmq/rmq_rpc/client"
 	"github.com/goccy/go-json"
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/credentials/insecure"
@@ -34,12 +35,17 @@ const (
 	// gRPC
 	grpcURL = host + ":8081"
 
-	// RabbitMQ RPC
-	rmqURL            = "amqp://guest:guest@rabbitmq:5672/"
+	// RPC configs
 	rpcServerExchange = "rpc_server"
 	rpcClientExchange = "rpc_client"
 	requests          = 10
 
+	// RabbitMQ RPC
+	rmqURL = "amqp://guest:guest@rabbitmq:5672/"
+
+	// RabbitMQ RPC
+	natsURL = "nats://guest:guest@nats:4222/"
+
 	// Test data
 	expectedOriginal = "текст для перевода"
 )
@@ -230,16 +236,59 @@ func TestClientGRPCV1(t *testing.T) {
 }
 
 // RabbitMQ RPC Client V1: getHistory.
-func TestClientRMQRPCV1(t *testing.T) {
-	rmqClient, err := client.New(rmqURL, rpcServerExchange, rpcClientExchange)
+func TestClientRMQRPCV1(t *testing.T) { //nolint: dupl,gocritic,nolintlint
+	client, err := rmqClient.New(rmqURL, rpcServerExchange, rpcClientExchange)
+	if err != nil {
+		t.Fatal("RabbitMQ RPC Client - init error - rmqClient.New", err)
+	}
+
+	defer func() {
+		err = client.Shutdown()
+		if err != nil {
+			t.Fatal("RabbitMQ RPC Client - shutdown error - client.RemoteCall", err)
+		}
+	}()
+
+	type Translation struct {
+		Source      string `json:"source"`
+		Destination string `json:"destination"`
+		Original    string `json:"original"`
+		Translation string `json:"translation"`
+	}
+
+	type historyResponse struct {
+		History []Translation `json:"history"`
+	}
+
+	for i := 0; i < requests; i++ {
+		var history historyResponse
+
+		err = client.RemoteCall("v1.getHistory", nil, &history)
+		if err != nil {
+			t.Fatal("RabbitMQ RPC Client - remote call error - client.RemoteCall", err)
+		}
+
+		if len(history.History) == 0 {
+			t.Fatal("History slice is empty, expected at least one entry")
+		}
+
+		if history.History[0].Original != expectedOriginal {
+			t.Fatalf("Original mismatch: expected %q, got %q", expectedOriginal, history.History[0].Original)
+		}
+	}
+}
+
+// NATS RPC Client V1: getHistory.
+func TestClientNATSRPCV1(t *testing.T) { //nolint: dupl,gocritic,nolintlint
+	client, err := natsClient.New(natsURL, rpcServerExchange)
 	if err != nil {
-		t.Fatal("RabbitMQ RPC Client - init error - client.New", err)
+		t.Fatal("NATS RPC Client - init error - natsClient.New", err)
 	}
 
 	defer func() {
-		err = rmqClient.Shutdown()
+		err = client.Shutdown()
 		if err != nil {
-			t.Fatal("RabbitMQ RPC Client - shutdown error - rmqClient.RemoteCall", err)
+			t.Fatal("NATS RPC Client - shutdown error - rmqClient.RemoteCall", err)
 		}
 	}()
 
@@ -257,9 +306,9 @@ func TestClientRMQRPCV1(t *testing.T) {
 	for i := 0; i < requests; i++ {
 		var history historyResponse
 
-		err = rmqClient.RemoteCall("v1.getHistory", nil, &history)
+		err = client.RemoteCall("v1.getHistory", nil, &history)
 		if err != nil {
-			t.Fatal("RabbitMQ RPC Client - remote call error - rmqClient.RemoteCall", err)
+			t.Fatal("NATS RPC Client - remote call error - rmqClient.RemoteCall", err)
 		}
 
 		if len(history.History) == 0 {