Skip to main content

Running Two HTTP Servers in Parallel

This tutorial will guide you through setting up and running multiple HTTP servers in parallel using the Gonyx Framework. You'll learn how to configure multiple servers in your application and route specific endpoints to different servers.

Prerequisites

Before starting, make sure you have:

  • Completed the Quick Start tutorial
  • Basic knowledge of Go programming language
  • Gonyx Framework installed on your system

Why Run Multiple HTTP Servers?

There are several scenarios where running multiple HTTP servers is beneficial:

  1. Separating concerns: You might want to have separate servers for public-facing APIs and admin interfaces
  2. Different security requirements: Running sensitive operations on a separate server with stricter security
  3. Performance optimization: Dedicated servers for specific high-traffic endpoints
  4. Versioning: Running different API versions on separate servers
  5. Different ports: Exposing services on different ports (e.g., HTTP and HTTPS)

Project Setup

First, create a new Gonyx project using the CLI:

gonyx init multi-server-demo --path .
cd multi-server-demo

This will create a basic project structure as described in the Quick Start tutorial.

Step 1: Configure Multiple HTTP Servers

The first step is to configure multiple HTTP servers in the configs/http.json file. By default, Gonyx generates a configuration with a single server. We'll modify this to include two servers.

Create or modify the configs/http.json file with the following content:

{
"default": "public",
"servers": [
{
"name": "public",
"addr": ":3000",
"versions": ["v1"],
"support_static": false,
"conf": {
"read_timeout": -1,
"write_timeout": -1,
"request_methods": ["ALL"]
},
"middlewares": {
"order": ["logger", "cors"],
"logger": {
"format": "> [${time}] ${status} - ${latency} ${method} ${path} ${queryParams}\n",
"time_format": "15:04:05",
"time_zone": "Local",
"time_interval": 500,
"output": "stdout"
},
"cors": {
"allow_all_origins": true,
"allow_origins": ["*"],
"allow_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"],
"allow_headers": [],
"allow_credentials": false
}
}
},
{
"name": "admin",
"addr": ":3001",
"versions": ["v1"],
"support_static": false,
"conf": {
"read_timeout": -1,
"write_timeout": -1,
"request_methods": ["ALL"]
},
"middlewares": {
"order": ["logger", "cors"],
"logger": {
"format": "> [ADMIN] [${time}] ${status} - ${latency} ${method} ${path}\n",
"time_format": "15:04:05",
"time_zone": "Local",
"time_interval": 500,
"output": "stdout"
},
"cors": {
"allow_all_origins": false,
"allow_origins": ["http://localhost:8080"],
"allow_methods": ["GET", "POST", "PUT", "DELETE"],
"allow_headers": ["Authorization"],
"allow_credentials": true
}
}
}
]
}

In this configuration:

  1. We've defined two HTTP servers:

    • public: Runs on port 3000, intended for public-facing APIs
    • admin: Runs on port 3001, intended for administrative operations
  2. The default server is set to "public", which means any routes that don't specify a server will be assigned to this one

  3. Each server has its own:

    • Port (addr)
    • Middleware configuration
    • CORS settings (more permissive for public, more restrictive for admin)

Step 2: Create Controllers for Each Server

Let's create two controllers: one for public APIs and one for admin operations. We'll modify the app/controller.go file:

package app

import (
"net/http"
gonyxhttp "github.com/Blocktunium/gonyx/pkg/http"
"github.com/gin-gonic/gin"
)

// ProductController - handles public product-related operations
type ProductController struct{}

// GetName - return the name of the controller
func (ctrl *ProductController) GetName() string { return "Product" }

// Routes - returning controller specific routes to be registered
func (ctrl *ProductController) Routes() []gonyxhttp.HttpRoute {
return []gonyxhttp.HttpRoute{
{
Method: gonyxhttp.MethodGet,
Path: "/products",
RouteName: "getAllProducts",
F: ctrl.GetAllProducts,
// No Servers parameter - will use the default server (public)
},
{
Method: gonyxhttp.MethodGet,
Path: "/products/:id",
RouteName: "getProductById",
F: ctrl.GetProductById,
// No Servers parameter - will use the default server (public)
},
}
}

// GetAllProducts - Get all products (public API)
func (ctrl *ProductController) GetAllProducts(c *gin.Context) {
products := []gin.H{
{"id": 1, "name": "Product 1", "price": 19.99},
{"id": 2, "name": "Product 2", "price": 29.99},
}
c.JSON(http.StatusOK, products)
}

// GetProductById - Get a product by ID (public API)
func (ctrl *ProductController) GetProductById(c *gin.Context) {
id := c.Param("id")
product := gin.H{"id": id, "name": "Product " + id, "price": 19.99}
c.JSON(http.StatusOK, product)
}

// AdminController - handles admin-only operations
type AdminController struct{}

// GetName - return the name of the controller
func (ctrl *AdminController) GetName() string { return "Admin" }

// Routes - returning controller specific routes to be registered
func (ctrl *AdminController) Routes() []gonyxhttp.HttpRoute {
return []gonyxhttp.HttpRoute{
{
Method: gonyxhttp.MethodGet,
Path: "/dashboard",
RouteName: "adminDashboard",
F: ctrl.GetDashboard,
Servers: []string{"admin"}, // Explicitly assign to admin server
},
{
Method: gonyxhttp.MethodPost,
Path: "/products",
RouteName: "createProduct",
F: ctrl.CreateProduct,
Servers: []string{"admin"}, // Explicitly assign to admin server
},
{
Method: gonyxhttp.MethodDelete,
Path: "/products/:id",
RouteName: "deleteProduct",
F: ctrl.DeleteProduct,
Servers: []string{"admin"}, // Explicitly assign to admin server
},
}
}

// GetDashboard - Admin dashboard (admin-only API)
func (ctrl *AdminController) GetDashboard(c *gin.Context) {
stats := gin.H{
"total_products": 2,
"total_orders": 10,
"revenue": 299.90,
}
c.JSON(http.StatusOK, stats)
}

// CreateProduct - Create a new product (admin-only API)
func (ctrl *AdminController) CreateProduct(c *gin.Context) {
var product struct {
Name string `json:"name"`
Price float64 `json:"price"`
}

if err := c.ShouldBindJSON(&product); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}

// In a real app, you would save to a database
newProduct := gin.H{
"id": 3,
"name": product.Name,
"price": product.Price,
"created": true,
}

c.JSON(http.StatusCreated, newProduct)
}

// DeleteProduct - Delete a product (admin-only API)
func (ctrl *AdminController) DeleteProduct(c *gin.Context) {
id := c.Param("id")
// In a real app, you would delete from a database
c.JSON(http.StatusOK, gin.H{"message": "Product " + id + " deleted successfully"})
}

In this example:

  1. The ProductController handles public-facing endpoints for viewing products

    • These routes don't specify a Servers parameter, so they will use the default server ("public")
  2. The AdminController handles admin-only operations like creating and deleting products

    • Each route explicitly specifies Servers: []string{"admin"} to assign it to the admin server

Step 3: Register Controllers in the App

Now, update the app/app.go file to register both controllers:

package app

import (
"github.com/Blocktunium/gonyx/pkg/engine"
)

// App - application engine structure
type App struct{}

// Init - initialize the app
func (app *App) Init() {
// Register both controllers
engine.RegisterRestfulController(&ProductController{})
engine.RegisterRestfulController(&AdminController{})
}

Step 4: Testing the Multiple Servers

Now let's run the application and test both servers:

go run main.go runserver

You should see logs indicating that Gonyx is starting two HTTP servers:

  • One on port 3000 (public)
  • One on port 3001 (admin)

Testing the Public Server

Use curl or your browser to test the public endpoints:

# Get all products (public server)
curl http://localhost:3000/products

# Get a single product (public server)
curl http://localhost:3000/products/1

Testing the Admin Server

Use curl or a tool like Postman to test the admin endpoints:

# Get admin dashboard (admin server)
curl http://localhost:3001/dashboard

# Create a new product (admin server)
curl -X POST http://localhost:3001/products \
-H "Content-Type: application/json" \
-d '{"name":"New Product","price":39.99}'

# Delete a product (admin server)
curl -X DELETE http://localhost:3001/products/1

Understanding Server Assignment

When defining routes in Gonyx, there are two ways to specify which server(s) should handle a route:

  1. Default server assignment: If you don't specify a Servers parameter in the HttpRoute, the route will be assigned to the default server as defined in your HTTP configuration ("default": "public").

  2. Explicit server assignment: Using the Servers parameter in the HttpRoute structure to explicitly list the servers that should handle the route.

Multiple Server Assignment

You can even assign a route to multiple servers by listing all target servers in the Servers array:

{
Method: gonyxhttp.MethodGet,
Path: "/health",
RouteName: "healthCheck",
F: ctrl.HealthCheck,
Servers: []string{"public", "admin"}, // Assign to both servers
}

This way, the same endpoint will be available on both servers.

Advanced Configuration

Server-Specific Middleware

Each server can have its own middleware configuration, as we've seen in our configuration with different logger formats and CORS settings. This allows you to implement different security policies, logging, or other behaviors for each server.

Authentication Strategies

A common pattern is to use different authentication strategies for different servers:

  • Public server: Maybe no auth, or simple API key authentication
  • Admin server: More secure JWT-based authentication with role verification

Conclusion

Running multiple HTTP servers in parallel with Gonyx is a powerful way to separate concerns in your application. By using the Servers parameter in your route definitions, you can explicitly control which server handles which endpoints.

This approach allows you to:

  • Isolate admin functionality from public APIs
  • Apply different security policies to different servers
  • Optimize each server for its specific purpose
  • Run services on different ports for different use cases

For more advanced use cases, you might consider using environment-specific configurations to use different server setups in development and production.