Microservice example application with Docker Swarm, Golang, gRPC, GraphQL, TypeScript and React - Part III
This post will cover products service and connecting it with gateway service and React app.
Updating GraphQL Schema and client integration
Let’s start with updating our GraphQL schema like below:
type Query {
sayHello: String!
products: [Product!] # new query
}
type Product {
id: ID!
title: String!
description: String!
price: Float!
image: String!
}
and run gqlgen generator go generate ./...
in gateway
folder.
Serving mock response for client
Update gateway/graph/schema.resolvers.go
file for mock product list response:
func (r *queryResolver) Products(ctx context.Context) ([]*model.Product, error) {
products := []*model.Product{
{
ID: "12e",
Title: "Camera",
Description: "Basic camera",
Price: 499.50,
Image: "https://via.placeholder.com/150",
},
{
ID: "24e",
Title: "Game console",
Description: "Game console. Video games.",
Price: 300.75,
Image: "https://via.placeholder.com/150",
},
{
ID: "43ert5",
Title: "Classical Novel",
Description: "Classical novel that we all like",
Price: 27,
Image: "https://via.placeholder.com/150",
},
}
return products, nil
}
With this we can now serve mock response for our React app via GraphQL.
## Integrating with React
Change directory to client
folder and run yarn run start
to start-up webpack-dev-server.
Create client/src/components/products
folder and ProductDetail.tsx
and ProductList.tsx
files following content:
import React from "react";
const ProductDetail: React.FC = () => {
return <div className="col">
<div className="card shadow-sm">
<img src="https://via.placeholder.com/150" />
<div className="card-body">
<p className="card-text">
Product description.
</p>
<div className="d-flex justify-content-between align-items-center">
<div className="btn-group">
<button type="button" className="btn btn-sm btn-outline-success">Add to Cart</button>
</div>
</div>
</div>
</div>
</div>;
}
export default ProductDetail;
import React from "react";
import ProductDetail from "./ProductDetail";
const ProductsList: React.FC = () => {
return <div className="album py-5 bg-light">
<div className="container">
<div className="row row-cols-1 row-cols-sm-5 row-cols-md-5 g-3">
<ProductDetail />
<ProductDetail />
<ProductDetail />
<ProductDetail />
<ProductDetail />
</div>
</div>
</div>;
}
export default ProductsList;
Use it in App.tsx
file in routing switch:
// .. rest of file
<Route path="/products">
<ProductsList />
</Route>
// .. rest of file
We created basic mock product list like below:
Integrate with GraphQL
We need a query to list products. Let’s create product-query.ts
file under products
folder.
import { gql } from '@apollo/client';
export const PRODUCTS_QUERY = gql`
query listProducts {
products {
id
title
description
price
image
}
}
`;
After this run yarn run codegen
to generate query hook for it.
Modify ProductList
component like below:
import React from "react";
import ProductDetail from "./ProductDetail";
import {useListProductsQuery} from "../../generated/graphql";
const ProductsList: React.FC = () => {
const {data, loading, error} = useListProductsQuery();
if (loading) return <h3>Loading...</h3>;
if (error) return <h3>Error occurred during displaying products. Please try again.</h3>
if (data && data.products) {
return <div className="album py-5 bg-light">
<div className="container">
<div className="row row-cols-1 row-cols-sm-5 row-cols-md-5 g-3">
{data.products.map(product => <ProductDetail key={product.id} product={product} /> )}
</div>
</div>
</div>;
}
return <h3>Error occurred during displaying products. Please try again.</h3>
}
export default ProductsList;
We need to modify ProductDetail
component in order to show product.
import React from "react";
import {useAppSelector} from "../../store/hooks";
import {selectedCurrentUser} from "../../store/auth/auth";
interface ProductProps {
id: string,
title: string,
description: string,
price: number,
image: string
}
interface ProductDetailProps {
product: ProductProps;
}
const ProductDetail: React.FC<ProductDetailProps> = ({ product }: ProductDetailProps) => {
const currentUser = useAppSelector(selectedCurrentUser); // for disabling unauthorized users clicking add to cart
return <div className="col">
<div className="card shadow-sm">
<img src={product.image} alt={product.title} />
<div className="card-body">
<p className="card-title">{product.title}</p>
<p className="card-text">
{product.description}
</p>
<div className="d-flex justify-content-between align-items-center">
<div className="btn-group">
<button type="button"
className="btn btn-sm btn-outline-success"
disabled={!currentUser.loggedIn}>
Add to Cart
</button>
</div>
<small className="text-muted">{product.price} EUR</small>
</div>
</div>
</div>
</div>;
}
export default ProductDetail;
Result is nice. We connected our React app to GraphQL server. Now we need to create product service and connect it to gateway.
Creating products service
In project’s root directory create folder called products
Inside products folder create product.proto
file.
syntax = "proto3";
package products;
option go_package = "product_grpc/product_grpc";
message ProductListRequest {}
message Product {
string id = 1;
string title = 2;
string description = 3;
float price = 4;
string image = 5;
}
message ProductList {
repeated Product products = 1;
}
service ProductService {
rpc ListProducts(ProductListRequest) returns (ProductList) {}
}
This describes our product server’s communication with gateway. To generate client and server run protoc --go_out=. --go-grpc_out=. products.proto
inside fake_store/products
folder. Create cmd/main.go
and cmd/server.go
files to serve product service.
// products/cmd/server.go
package main
import (
"context"
"github.com/yigitsadic/fake_store/products/product_grpc/product_grpc"
)
type server struct {
product_grpc.UnimplementedProductServiceServer
}
func (s *server) ListProducts(context.Context, *product_grpc.ProductListRequest) (*product_grpc.ProductList, error) {
products := []*product_grpc.Product{
{
Id: "825c2ca8-cfeb-4ba4-8b34-fb93f7958fa8",
Title: "Cornflakes",
Price: 6.94,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "46541671-d9dd-4e99-9f40-c807e1b14f11",
Title: "Vaccum Bag - 14x20",
Price: 4.97,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "c3af5841-4cfe-4ba0-874b-7c8ced576357",
Title: "Mustard - Dijon",
Price: 1.25,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "966a9098-3252-4a43-9776-dd7f66e09d91",
Title: "Cheese - Le Cru Du Clocher",
Price: 1.69,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "0fef08f2-cc56-4fd7-9137-b0ab561bc7a1",
Title: "Beef - Striploin",
Price: 2.71,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "9f932b92-3433-4be2-8302-7ac4901c97d6",
Title: "Beef - Bones, Marrow",
Price: 8.73,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "6bf9959e-cf2c-4039-9a31-30a9e90e8d7c",
Title: "V8 Pet",
Price: 6.61,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "4f2a902a-446f-41da-9d12-521f9c83c94a",
Title: "Sauce - Fish 25 Ozf Bottle",
Price: 2.49,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "3497030f-7239-4fce-bb73-f446e4fedc10",
Title: "Beef - Rib Eye Aaa",
Price: 6.38,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "49d5f82e-d636-4d6c-8508-5429db7fd4b1",
Title: "Muffin Mix - Banana Nut",
Price: 5.68,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "ee66f7e3-4bdd-4298-b790-43a2431c77ab",
Title: "Dawn Professionl Pot And Pan",
Price: 4.89,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "992d3766-6022-4ee1-847e-f293f2488951",
Title: "Jameson - Irish Whiskey",
Price: 1.12,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "95ca1986-9e39-485e-942e-927ac91aecde",
Title: "Bread Fig And Almond",
Price: 2.58,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "eb46937c-12f5-4b9b-8ffa-7cf20871fbaf",
Title: "Vinegar - White",
Price: 4.16,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
{
Id: "c03020f2-fbf2-463d-9003-15e1901dc47a",
Title: "Bouq All Italian - Primerba",
Price: 4.33,
Description: "Lorem ipsum dolor sit amet",
Image: "https://via.placeholder.com/150",
},
}
return &product_grpc.ProductList{Products: products}, nil
}
// products/cmd/main.go
package main
import (
"fmt"
"github.com/yigitsadic/fake_store/products/product_grpc/product_grpc"
"google.golang.org/grpc"
"log"
"net"
)
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 9000))
if err != nil {
log.Fatalf("failed to listen: %v\n", err)
}
grpcServer := grpc.NewServer()
s := server{}
product_grpc.RegisterProductServiceServer(grpcServer, &s)
log.Println("Started to serve product grpc")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve due to %s\n", err)
}
}
And we need a dockerfile. It’s copy & paste at this moment.
FROM golang:1.17.0-alpine3.13 as compiler
WORKDIR /src/app
COPY go.mod go.sum ./
COPY products products
RUN go build -o product_service ./products/cmd/
FROM alpine:3.13
WORKDIR /src
COPY --from=compiler /src/app/product_service /src/app
CMD ["/src/app"]
Connecting products service with gateway
In gateway/graph
folder find resolver.go
and add product service client to Resolver as dependency.
//go:generate go run github.com/99designs/gqlgen
package graph
import (
"github.com/yigitsadic/fake_store/auth/client/client"
"github.com/yigitsadic/fake_store/products/product_grpc/product_grpc"
)
type Resolver struct {
AuthClient client.AuthServiceClient
ProductsClient product_grpc.ProductServiceClient // Product service
}
And in schema.resolvers.go
file change Products like below:
// rest of file
func (r *queryResolver) Products(ctx context.Context) ([]*model.Product, error) {
var products []*model.Product
productResp, err := r.ProductsClient.ListProducts(ctx, nil)
if err != nil {
return nil, err
}
for _, product := range productResp.Products {
products = append(products, &model.Product{
ID: product.Id,
Title: product.Title,
Description: product.Description,
Price: float64(product.Price),
Image: product.Image,
})
}
return products, nil
}
// rest of file
In initialization, let’s add client to resolver:
// gateway/cmd/main.go
productsConnection, productClient := acquireProductsConnection()
defer productsConnection.Close()
resolver := graph.Resolver{
AuthClient: authClient,
ProductsClient: productClient, // our new service
}
// bottom of file
func acquireProductsConnection() (*grpc.ClientConn, product_grpc.ProductServiceClient) {
conn, err := grpc.Dial("products:9000", grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalln("Unable to acquire products connection")
}
c := product_grpc.NewProductServiceClient(conn)
return conn, c
}
We need to update gateway service’s Dockerfile too:
COPY products products
Adding service to swarm
Add products service to docker-compose.yml
and run docker-compose build
and docker-compose up
products:
build:
context: .
dockerfile: ./products/Dockerfile
With this refactor we can see products from products service. But there is a small problem. Products’ prices look bad but I don’t care it right now.
What’s next?
I will cover cart service and connecting it with gateway service and React app on part IV.
À la prochaine !