Go REST gRPC API?

I’ll try to make a quick summary of everything involved but I won’t bother with where things should be and some relations: you should really look up and read about the different pieces, and then try to follow the repo’s structure to see how everything adds up.

First, you’ll want to create some protos describing your services and their messages, e.g.:

syntax = "proto3";

package api;

import "google/api/annotations.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";

// UserService is the service managing the user access.
service UserService {

	// Get data for a particular user.
	rpc Get(GetUserRequest) returns (GetUserResponse) {
		option(google.api.http) = {
			get: "/api/users/{id}"
		};
	}

	// Create a new user.
	rpc Create(CreateUserRequest) returns (CreateUserResponse) {
		option(google.api.http) = {
			post: "/api/users"
			body: "*"
		};
	}
}


message User {
	// User ID.
	// Will be set automatically on create.
	int64 id = 1;

	// Username of the user.
	string username = 2;

	// The session timeout, in minutes.
	int32 session_ttl = 3 [json_name = "sessionTTL"];

	// Set to true to make the user a global administrator.
	bool is_admin = 4;

	// Set to false to disable the user.
	bool is_active = 5;

	// E-mail of the user.
	string email = 6;

	// Optional note to store with the user.
	string note = 7;
}

message CreateUserRequest {
	// User object to create.
	User user = 1;

	// Password of the user.
	string password = 2;

	// Add the user to the following organizations.
	repeated UserOrganization organizations = 3;
}

message CreateUserResponse {
	// User ID.
	int64 id = 1;
}

message GetUserRequest {
	// User ID.
	int64 id = 1;
}

message GetUserResponse {
	// User object.
	User user = 1;

	// Created at timestamp.
	google.protobuf.Timestamp created_at = 2;

	// Last update timestamp.
	google.protobuf.Timestamp updated_at = 3;
}

After that you’ll want to generate your gRPC code from it and optionally the REST gateway and swagger definitions, which could be achieved with a script like this after you’ve installed protoc (protobuffer compiler) and the needed plugins (https://github.com/grpc-ecosystem/grpc-gateway):

#!/usr/bin/env bash

GRPC_GW_PATH=`go list -f '{{ .Dir }}' github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway`
GRPC_GW_PATH="${GRPC_GW_PATH}/../third_party/googleapis"

# generate the gRPC code
protoc -I../vendor -I${GRPC_GW_PATH} -I. --go_out=plugins=grpc:. \
    user.proto \
    other_model.proto

# generate the JSON interface code
protoc -I../vendor -I${GRPC_GW_PATH} -I. --grpc-gateway_out=logtostderr=true:. \
    user.proto \
    other_model.proto

# generate the swagger definitions
protoc -I../vendor -I${GRPC_GW_PATH} -I. --swagger_out=json_names_for_fields=true:./swagger \
    device.proto \

# merge the swagger code into one file
go run swagger/main.go swagger > ../static/swagger/api.swagger.json

If we run the script it’ll generate our gRPC code, the transalation to a JSON REST API and a nice swagger definition to use at the frontend. Great! Now we need to implement real logic for the endpoints:

// UserAPI exports the User related functions.
type UserAPI struct {
	validator auth.Validator
}

// NewUserAPI creates a new UserAPI.
func NewUserAPI(validator auth.Validator) *UserAPI {
	return &UserAPI{
		validator: validator,
	}
}

// Create creates the given user.
func (a *UserAPI) Create(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
	if req.User == nil {
		return nil, grpc.Errorf(codes.InvalidArgument, "user must not be nil")
	}

	if err := a.validator.Validate(ctx,
		auth.ValidateUsersAccess(auth.Create)); err != nil {
		return nil, grpc.Errorf(codes.Unauthenticated, "authentication failed: %s", err)
	}

	// validate if the client has admin rights for the given organizations
	// to which the user must be linked
	for _, org := range req.Organizations {
		if err := a.validator.Validate(ctx,
			auth.ValidateIsOrganizationAdmin(org.OrganizationId)); err != nil {
			return nil, grpc.Errorf(codes.Unauthenticated, "authentication failed: %s", err)
		}
	}

	user := storage.User{
		Username:   req.User.Username,
		SessionTTL: req.User.SessionTtl,
		IsAdmin:    req.User.IsAdmin,
		IsActive:   req.User.IsActive,
		Email:      req.User.Email,
		Note:       req.User.Note,
	}

	isAdmin, err := a.validator.GetIsAdmin(ctx)
	if err != nil {
		return nil, helpers.ErrToRPCError(err)
	}

	if !isAdmin {
		// non-admin users are not able to modify the fields below
		user.IsAdmin = false
		user.IsActive = true
		user.SessionTTL = 0
	}

	var userID int64

	err = storage.Transaction(func(tx sqlx.Ext) error {
		userID, err = storage.CreateUser(tx, &user, req.Password)
		if err != nil {
			return err
		}

		for _, org := range req.Organizations {
			if err := storage.CreateOrganizationUser(tx, org.OrganizationId, userID, org.IsAdmin); err != nil {
				return err
			}
		}

		return nil
	})
	if err != nil {
		return nil, helpers.ErrToRPCError(err)
	}

	return &pb.CreateUserResponse{Id: userID}, nil
}

// Get returns the user matching the given ID.
func (a *UserAPI) Get(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
	if err := a.validator.Validate(ctx,
		auth.ValidateUserAccess(req.Id, auth.Read)); err != nil {
		return nil, grpc.Errorf(codes.Unauthenticated, "authentication failed: %s", err)
	}

	user, err := storage.GetUser(storage.DB(), req.Id)
	if err != nil {
		return nil, helpers.ErrToRPCError(err)
	}

	resp := pb.GetUserResponse{
		User: &pb.User{
			Id:         user.ID,
			Username:   user.Username,
			SessionTtl: user.SessionTTL,
			IsAdmin:    user.IsAdmin,
			IsActive:   user.IsActive,
			Email:      user.Email,
			Note:       user.Note,
		},
	}

	resp.CreatedAt, err = ptypes.TimestampProto(user.CreatedAt)
	if err != nil {
		return nil, helpers.ErrToRPCError(err)
	}
	resp.UpdatedAt, err = ptypes.TimestampProto(user.UpdatedAt)
	if err != nil {
		return nil, helpers.ErrToRPCError(err)
	}

	return &resp, nil
}

Now we need to serve the gRPC service and the HTTP gateway API:

func setupAPI(conf config.Config) error {
	validator := auth.NewJWTValidator(storage.DB(), "HS256", jwtSecret)
	rpID, err := uuid.FromString(conf.ApplicationServer.ID)
	if err != nil {
		return errors.Wrap(err, "application-server id to uuid error")
	}

	grpcOpts := helpers.GetgRPCLoggingServerOptions()
	sizeOpts := []grpc.ServerOption{
		grpc.MaxRecvMsgSize(math.MaxInt32),
		grpc.MaxSendMsgSize(math.MaxInt32),
		grpc.MaxMsgSize(math.MaxInt32),
	}
	grpcOpts = append(grpcOpts, sizeOpts...)
	grpcServer := grpc.NewServer(grpcOpts...)
	api.RegisterUserServiceServer(grpcServer, NewUserAPI(validator))
	api.RegisterOtherModelServiceServer(grpcServer, NewOtherModelAPI(validator))
// setup the client http interface variable
	// we need to start the gRPC service first, as it is used by the
	// grpc-gateway
	var clientHTTPHandler http.Handler

	// switch between gRPC and "plain" http handler
	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 2 && strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
			grpcServer.ServeHTTP(w, r)
		} else {
			if clientHTTPHandler == nil {
				w.WriteHeader(http.StatusNotImplemented)
				return
			}

			if corsAllowOrigin != "" {
				w.Header().Set("Access-Control-Allow-Origin", corsAllowOrigin)
				w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
				w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, Grpc-Metadata-Authorization")

				if r.Method == "OPTIONS" {
					return
				}
			}

			clientHTTPHandler.ServeHTTP(w, r)
		}
	})

	// start the API server
	go func() {
		log.WithFields(log.Fields{
			"bind":     bind,
			"tls-cert": tlsCert,
			"tls-key":  tlsKey,
		}).Info("api/external: starting api server")

		if tlsCert == "" || tlsKey == "" {
			log.Fatal(http.ListenAndServe(bind, h2c.NewHandler(handler, &http2.Server{})))
		} else {
			log.Fatal(http.ListenAndServeTLS(
				bind,
				tlsCert,
				tlsKey,
				h2c.NewHandler(handler, &http2.Server{}),
			))
		}
	}()

	// give the http server some time to start
	time.Sleep(time.Millisecond * 100)

	// setup the HTTP handler
	clientHTTPHandler, err = setupHTTPAPI(conf)
	if err != nil {
		return err
	}

	return nil
}

//This serves and routes the HTTP API and static files (e.g., the React frontend files).
func setupHTTPAPI(conf config.Config) (http.Handler, error) {
	r := mux.NewRouter()

	// setup json api handler
	jsonHandler, err := getJSONGateway(context.Background())
	if err != nil {
		return nil, err
	}

	log.WithField("path", "/api").Info("api/external: registering rest api handler and documentation endpoint")
	r.HandleFunc("/api", func(w http.ResponseWriter, r *http.Request) {
		data, err := static.Asset("swagger/index.html")
		if err != nil {
			log.WithError(err).Error("get swagger template error")
			w.WriteHeader(http.StatusInternalServerError)
			return
		}
		w.Write(data)
	}).Methods("get")
	r.PathPrefix("/api").Handler(jsonHandler)

	// setup static file server
	r.PathPrefix("/").Handler(http.FileServer(&assetfs.AssetFS{
		Asset:     static.Asset,
		AssetDir:  static.AssetDir,
		AssetInfo: static.AssetInfo,
		Prefix:    "",
	}))

	return wsproxy.WebsocketProxy(r), nil
}

//This registers the gRPC handlers JSON gateways to pass them over to the HTTP API.
func getJSONGateway(ctx context.Context) (http.Handler, error) {
	// dial options for the grpc-gateway
	var grpcDialOpts = []grpc.DialOption{
		grpc.WithDefaultCallOptions([]grpc.CallOption{
			grpc.MaxCallRecvMsgSize(math.MaxInt32),
			grpc.MaxCallSendMsgSize(math.MaxInt32),
		}...),
	}

	if tlsCert == "" || tlsKey == "" {
		grpcDialOpts = append(grpcDialOpts, grpc.WithInsecure())
	} else {
		b, err := ioutil.ReadFile(tlsCert)
		if err != nil {
			return nil, errors.Wrap(err, "read external api tls cert error")
		}
		cp := x509.NewCertPool()
		if !cp.AppendCertsFromPEM(b) {
			return nil, errors.Wrap(err, "failed to append certificate")
		}
		grpcDialOpts = append(grpcDialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
			// given the grpc-gateway is always connecting to localhost, does
			// InsecureSkipVerify=true cause any security issues?
			InsecureSkipVerify: true,
			RootCAs:            cp,
		})))
	}

	bindParts := strings.SplitN(bind, ":", 2)
	if len(bindParts) != 2 {
		log.Fatal("get port from bind failed")
	}
	apiEndpoint := fmt.Sprintf("localhost:%s", bindParts[1])

	mux := runtime.NewServeMux(runtime.WithMarshalerOption(
		runtime.MIMEWildcard,
		&runtime.JSONPb{
			EnumsAsInts:  false,
			EmitDefaults: true,
		},
	))

	if err := pb.RegisterUserServiceHandlerFromEndpoint(ctx, mux, apiEndpoint, grpcDialOpts); err != nil {
		return nil, errors.Wrap(err, "register user handler error")
	}
	if err := pb.RegisterotherModelServiceHandlerFromEndpoint(ctx, mux, apiEndpoint, grpcDialOpts); err != nil {
		return nil, errors.Wrap(err, "register other model handler error")
	}
 
	return mux, nil
}

Finally, in your React frontend you may have stores for each service, e.g.:


import { EventEmitter } from "events";

import Swagger from "swagger-client";

import sessionStore from "./SessionStore";
import {checkStatus, errorHandler } from "./helpers";
import dispatcher from "../dispatcher";


class UserStore extends EventEmitter {
  constructor() {
    super();
    this.swagger = new Swagger("/swagger/user.swagger.json", sessionStore.getClientOpts());
  }

  create(user, password, organizations, callbackFunc) {
    this.swagger.then(client => {
      client.apis.UserService.Create({
        body: {
          organizations: organizations,
          password: password,
          user: user,
        },
      })
      .then(checkStatus)
      .then(resp => {
        this.notify("created");
        callbackFunc(resp.obj);
      })
      .catch(errorHandler);
    });
  }

  get(id, callbackFunc) {
    this.swagger.then(client => {
      client.apis.UserService.Get({
        id: id,
      })
      .then(checkStatus)
      .then(resp => {
        callbackFunc(resp.obj);
      })
      .catch(errorHandler);
    });
  }
}

And then those are used in your frontend views:

import React, { Component } from "react";
import UserForm from "./UserForm";
import UserStore from "../../stores/UserStore";

class CreateUser extends Component {
  constructor() {
    super();
    this.onSubmit = this.onSubmit.bind(this);
  }

  onSubmit(user) {
    UserStore.create(user, user.password, [], resp => {
      this.props.history.push("/users");
    });
  }

  render() {
    return(
              <UserForm
                submitLabel="Create user"
                onSubmit={this.onSubmit}
              />
    );
  }
}

export default withRouter(CreateUser);

Now, this is in no way a guide on how to do this, just a general idea of what pieces need to be coded: proto definitions, endpoints logic, serving code and frontend. Also, there’s no need to use React, swagger definitions to specify the API are unnecessary sugar, etc. I’m sure I’ve seen a bunch of guides or tutorials posted at reddit and other sites which will probably be more helpful, just look up some of those regarding the actual technologies that you want to use.

Good luck!