In this example we are going to implement a RESTful public HTTP API that communicates with our internal TCP server with a HTTP client for storing data. The process is very simple as shown below.


  1. User sends HTTP request to our public RESTful HTTP API.

  2. RESTful HTTP API forwards request to internal TCP server with a HTTP client.

  3. TCP server stores the data.

  4. TCP server returns response to TCP client over HTTP network.

  5. RESTful HTTP API responds to user.

User (HTTP request) -> RESTful HTTP API (HTTP request) -> TCP Server (TCP response) -> RESTful HTTP API (HTTP response) -> User

Improvements


You can timeout HTTP request at client side in terms of possible improvements.


Client structure


├── cmd
│   └── client
│   └── main.go
└── internal
├── pkg
│   ├── client
│   │   └── client.go
│   ├── router
│   │   └── router.go
│   └── server
│   └── server.go
└── user
└── create.go

Client files


main.go


package main

import (
"log"
"time"

"github.com/inanzzz/client/internal/pkg/client"
"github.com/inanzzz/client/internal/pkg/router"
"github.com/inanzzz/client/internal/pkg/server"
)

func main() {
clt := client.New("http://0.0.0.0:9999/api/v1", 3*time.Second)

rtr := router.New()
rtr.RegisterUser(clt)

srv := server.New("0.0.0.0:8888", rtr.Handler)
log.Fatalln(srv.ListenAndServe())
}

client.go


package client

import (
"context"
"net/http"
"strings"
"time"
)

type Client struct {
baseURL string
timeout time.Duration
client http.Client
}

func New(baseURL string, timeout time.Duration) Client {
return Client{
baseURL: baseURL,
timeout: timeout,
client: http.Client{},
}
}

func (c Client) Request(ctx context.Context, method, url, body string, headers map[string]string) (*http.Response, error) {
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()

req, err := http.NewRequestWithContext(ctx, method, c.baseURL+url, strings.NewReader(body))
if err != nil {
return nil, err
}

for k, v := range headers {
req.Header.Add(k, v)
}

res, err := c.client.Do(req)
if err != nil {
return nil, err
}

return res, nil
}

router.go


package router

import (
"net/http"

"github.com/inanzzz/client/internal/pkg/client"
"github.com/inanzzz/client/internal/user"
"github.com/julienschmidt/httprouter"
)

type Router struct {
Handler *httprouter.Router
}

func New() *Router {
rtr := httprouter.New()
rtr.RedirectTrailingSlash = false
rtr.RedirectFixedPath = false

return &Router{
Handler: rtr,
}
}

func (r *Router) RegisterUser(clt client.Client) {
r.Handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(clt).Handle)
}

server.go


package server

import (
"net/http"
)

func New(adr string, hnd http.Handler) http.Server {
return http.Server{
Addr: adr,
Handler: hnd,
}
}

create.go


package user

import (
"context"
"encoding/json"
"io/ioutil"
"log"
"net/http"

"github.com/inanzzz/client/internal/pkg/client"
)

type CreateRequestBody struct {
Username string `json:"username"`
Password string `json:"password"`
}

type Create struct {
client client.Client
}

func NewCreate(clt client.Client) Create {
return Create{
client: clt,
}
}

// POST /api/v1/users
func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
var req CreateRequestBody

// Map HTTP request to request model
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Printf("unable to decode request: %v", err)
return
}

// Convert request model to client request body
body, err := json.Marshal(req)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
log.Printf("unable to marshal request: %v", err)
return
}

// Store request model
res, err := c.client.Request(context.Background(), http.MethodPost, "/users", string(body), nil)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Printf("unable to store request: %v", err)
return
}
defer res.Body.Close()

// Check if the response code is an expected value
if res.StatusCode != http.StatusOK {
w.WriteHeader(http.StatusInternalServerError)
log.Print("unable to store data")
return
}

// Convert response model to HTTP response
data, err := ioutil.ReadAll(res.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
log.Print("unable to read client response")
return
}

// Respond
w.Header().Add("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(data)
}

Server structure


├── cmd
│   └── server
│   └── main.go
└── internal
├── pkg
│   ├── http
│   │   ├── router.go
│   │   └── server.go
│   └── tcp
│   └── listener.go
└── user
└── create.go

Files


main.go


package main

import (
"log"

"github.com/inanzzz/client/internal/pkg/http"
"github.com/inanzzz/client/internal/pkg/tcp"

nethttp "net/http"
)

func main() {
listener, err := tcp.NewListener("0.0.0.0:9999")
if err != nil {
log.Fatalln(err)
}

rtr := http.NewRouter()
rtr.RegisterUser(listener)

srv := http.NewServer(*rtr)
if err := srv.Serve(listener); err == nethttp.ErrServerClosed {
log.Println("Shutdown")
}
}

router.go


package http

import (
"net"
"net/http"

"github.com/inanzzz/client/internal/user"
"github.com/julienschmidt/httprouter"
)

type Router struct {
handler *httprouter.Router
}

func NewRouter() *Router {
rtr := httprouter.New()
rtr.RedirectTrailingSlash = false
rtr.RedirectFixedPath = false

return &Router{
handler: rtr,
}
}

func (r *Router) RegisterUser(tcpListener net.Listener) {
r.handler.HandlerFunc(http.MethodPost, "/api/v1/users", user.NewCreate(tcpListener).Handle)
}

server.go


package http

import (
"net/http"
)

func NewServer(router Router) *http.Server {
return &http.Server{
Handler: router.handler,
}
}

listener.go


package tcp

import (
"net"
)

func NewListener(address string) (net.Listener, error) {
return net.Listen("tcp", address)
}

create.go


You actually don't need net.Listener below.


package user

import (
"fmt"
"log"
"net"
"net/http"
"time"

"github.com/google/uuid"
)

type Create struct {
tcpListener net.Listener
}

func NewCreate(tcpListener net.Listener) Create {
return Create{
tcpListener: tcpListener,
}
}

func (c Create) Handle(w http.ResponseWriter, r *http.Request) {
log.Println(r.Header.Get("X-Request-ID"))

body := fmt.Sprintf(`{"code":%d,"data":{"id":"%s","created_at":"%s"}}`,
http.StatusOK,
uuid.New().String(),
time.Now().UTC().Format(time.RFC3339),
)

w.Header().Add("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write([]byte(body))
}

Test


Assuming that your server and client are running.


curl -i -X POST -d '{"username":"username","password":"password"}' "http://0.0.0.0:8888/api/v1/users"

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Fri, 05 Jun 2020 17:05:22 GMT
Content-Length: 101

{"code":200,"data":{"id":"7af224a1-5f23-476d-a0a3-92278b0169cc","created_at":"2020-06-05T17:05:22Z"}}

1000 HTTP requests have been served within 20/22 seconds which equals to 1 request per 20/22 milliseconds.