When to use RESTful API
For most startups, we should focus more on delivering the products in the early stage of business. The monolithic services have the advantages of simple architecture, easy deployment, and better development productivity, which can help us achieve the product requirements quickly. While we use monolithic services to deliver products quickly, we also need to reserve the possibility for business incresement, so we usually split different business modules clearly in monolithic services.
Shopping mall monolithic service architecture
We take the mall as an example to build a monolithic service. The mall service is generally relatively complex and consists of multiple modules, the more important modules include account, product and order modules, etc. Each module will have its own independent business logic, and each module will also depend on some others. For example, the order module and the product module will depend on the account module. In the monolithic application this kind of dependency is usually accomplished by method calls between modules. Monolithic services generally share storage resources, such as MySQL
and Redis
.
The overall architecture of monolithic services is relatively simple, which is also the advantage of monolithic services. Customer requests are parsed through DNS
and forwarded to the mall's backend services through Nginx
. Mall services are deployed on cloud hosts. In order to achieve greater throughput and high availability, the service will generally deployed with multiple copies. This simple architecture can carry high throughput if well optimized.
For example, a request for order details interface /order/detail
is routed to the order module, which relies on the account module and the product module to compose the complete order details back to the user, and multiple modules in a single service generally share the database and cache.
Monolithic Service
The next section describes how to quickly implement a mall monolithic service based on go-zero
. Devs who have used go-zero
know that we provide an API
format file to describe the Restful API
, and then we can generate the corresponding code by goctl
with one command, we just need to fill in the corresponding business logic in the logic
files. The mall service contains several modules, and in order to make the modules independent from each other, different modules are defined by separate API
s, but all the API
s are defined for the same service (mall-api)
.
Create user.api
, order.api
, product.api
and mall.api
in the api
directory, where mall.api
is the aggregated api
file. Other api
files are imported via import
directives.
api
|-- mall.api
|-- order.api
|-- product.api
|-- user.api
Mall API Definition
mall.api
is defined as follows, where syntax = "v1"
means that this is the v1
syntax of zero-api
.
syntax = "v1"
import "user.api"
import "order.api"
import "product.api"
Account module API definition
- View user details
- Get all orders for a user
user.api
is defined as follows.
syntax = "v1"
type (
UserRequest {
ID int64 `path:"id"`
}
UserReply {
ID int64 `json:"id"`
Name string `json:"name"`
Balance float64 `json:"balance"`
}
UserOrdersRequest {
ID int64 `path:"id"`
}
UserOrdersReply {
ID string `json:"id"`
State uint32 `json:"state"`
CreateAt string `json:"create_at"`
}
)
service mall-api {
@handler UserHandler
get /user/:id (UserRequest) returns (UserReply)
@handler UserOrdersHandler
get /user/:id/orders (UserOrdersRequest) returns (UserOrdersReply)
}
Order module API definition
- Get order details
- Generate orders
order.api
is defined as follows.
syntax = "v1"
type (
OrderRequest {
ID string `path:"id"`
}
OrderReply {
ID string `json:"id"`
State uint32 `json:"state"`
CreateAt string `json:"create_at"`
}
OrderCreateRequest {
ProductID int64 `json:"product_id"`
}
OrderCreateReply {
Code int `json:"code"`
}
)
service mall-api {
@handler OrderHandler
get /order/:id (OrderRequest) returns (OrderReply)
@handler OrderCreateHandler
post /order/create (OrderCreateRequest) returns (OrderCreateReply)
}
Product module API definition
- View product details
product.api
is defined as follows.
syntax = "v1"
type ProductRequest {
ID int64 `path:"id"`
}
type ProductReply {
ID int64 `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Count int64 `json:"count"`
}
service mall-api {
@handler ProductHandler
get /product/:id (ProductRequest) returns (ProductReply)
}
Generating the monolithic service
With the API
already defined, generating a service with the API
becomes very simple, we use goctl
to generate the monolithic service code.
$ goctl api go -api api/mall.api -dir .
The generated code is structured as follows.
.
├── api
│ ├── mall.api
│ ├── order.api
│ ├── product.api
│ └── user.api
├── etc
│ └── mall-api.yaml
├─ internal
│ ├── config
│ │ └── config.go
│ ├── handler
│ │ ├── ordercreatehandler.go
│ │ ├── orderhandler.go
│ │ ├── producthandler.go
│ │ ├── routes.go
│ │ ├── userhandler.go
│ │ └─ userordershandler.go
│ ├─ logic
│ │ ├─ ordercreatelogic.go
│ │ ├── orderlogic.go
│ │ ├── productlogic.go
│ │ ├── userlogic.go
│ │ └── userorderslogic.go
│ ├── svc
│ │ └── servicecontext.go
│ └── types
│ └── types.go
└── mall.go
Let's explain the generated files.
-
api
: holds theAPI
description file -
etc
: used to define the project configuration, all configuration items can be written inmall-api.yaml
-
internal/config
: the configuration definition of the service -
internal/handler
: the implementation of thehandler
corresponding to the routes defined in theAPI
file -
internal/logic
: used to put the business logic corresponding to each route, the reason for the distinction betweenhandler
andlogic
is to make the business processing part as less dependent as possible, to separateHTTP requests
from the logic processing code, and to facilitate the subsequent splitting intoRPC service
-
internal/svc
: used to define the dependencies of the business logic processing, we can create the dependent resources in themain
function and pass them tohandler
andlogic
viaServiceContext
-
internal/types
: defines theAPI
request and response data structures -
mall.go
: the file where themain
function is located, with the same name as theservice
in theAPI
definition, minus the-api
suffix
The generated service can be run without any modification:
$ go run mall.go
Starting server at 0.0.0.0:8888...
Implementing the business logic
Next, let's implement the business logic. The logic will be simple for demonstration purposes, not real business logic.
First, let's implement the logic of getting all orders for users. Since there is no order-related information in the user module, we need to rely on the order module to query the orders of users, so we add a dependency on OrderLogic
in UserOrdersLogic
.
type UserOrdersLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
orderLogic *OrderLogic
}
func NewUserOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UserOrdersLogic {
return &UserOrdersLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
orderLogic: NewOrderLogic(ctx, svcCtx),
}
}
Implement a method in OrderLogic
to query all orders based on user id
func (l *OrderLogic) ordersByUser(uid int64) ([]*types.OrderReply, error) {
if uid == 123 {
// It should actually be queried from database or cache
return []*types.OrderReply{
{
ID: "236802838635",
State: 1,
CreateAt: "2022-5-12 22:59:59",
},
{
ID: "236802838636",
State: 1,
CreateAt: "2022-5-10 20:59:59",
},
}, nil
}
return nil, nil
}
Call the ordersByUser
method in the UserOrders
method of UserOrdersLogic
.
func (l *UserOrdersLogic) UserOrders(req *types.UserOrdersRequest) (*types.UserOrdersReply, error) {
orders, err := l.orderLogic.ordersByUser(req.ID)
if err ! = nil {
return nil, err
}
return &types.UserOrdersReply{
Orders: orders,
}, nil
}
At this point we restart the mall-api
service and request all the user's orders in the browser.
http://localhost:8888/user/123/orders
The return result is as follows, as we expected
{
"orders": [
{
"id": "236802838635",
"state": 1,
"create_at": "2022-5-12 22:59:59"
},
{
"id": "236802838636",
"state": 1,
"create_at": "2022-5-10 20:59:59"
}
]
}
Next we'll implement the logic for creating an order. To create an order we first need to see if the item in stock is enough, so we need to rely on the item module in the order module.
type OrderCreateLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
productLogic *ProductLogic
productLogic *ProductLogic
}
func NewOrderCreateLogic(ctx context.Context, svcCtx *svc.ServiceContext) *OrderCreateLogic {
return &OrderCreateLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
productLogic: NewProductLogic(ctx, svcCtx),
}
}
The logic for creating an order is as follows.
const (
success = 0
failure = -1
)
func (l *OrderCreateLogic) OrderCreate(req *types.OrderCreateRequest) (*types.OrderCreateReply, error) {
product, err := l.productLogic.productByID(req.ProductID)
if err ! = nil {
return nil, err
}
if product.Count > 0 {
return &types.OrderCreateReply{Code: success}, nil
}
return &types.OrderCreateReply{Code: failure}, nil
}
The logic of the dependent product module is as follows.
func (l *ProductLogic) Product(req *types.ProductRequest) (*types.ProductReply, error) {
return l.productByID(req.ID)
}
func (l *ProductLogic) productByID(id int64) (*types.ProductReply, error) {
return &types.ProductReply{
ID: id,
Name: "apple watch 3",
Price: 3333.33,
Count: 99,
}, nil
}
The above shows that using go-zero
to develop a monolithic service is very simple, which helps us to develop quickly. And we also separated modules, which also provides the possibility of changing to microservices later.
Summary
The above example shows that it is very simple to use go-zero
to develop monolithic services. You only need to define the api
file, and then the goctl
tool can automatically generate the project code. We only need to fill in the business logic code in the logic package. In this article we just demonstrated how to quickly develop monolithic services based on go-zero
, which does not involve databases. In fact, goctl
can also generate CRUD
and cache
code with one command.
And for different business scenarios, customization can also be achieved through customizing templates. And customized templates can be shared within the team through remote git
repositories, which can be very efficient for team collaboration.
Project address
https://github.com/zeromicro/go-zero
Welcome to use go-zero
and star to support us!
Top comments (4)
Fantastic article!
What software did you use to create those diagram?
Thanks!
The tool is onemodel.app
Thanks Kevin!!
hi Kevin,
In user.api, the UserOrdersReply struct should be:
UserOrdersReply {
Orders []*OrderReply
json:"orders"
}
btw It's better if you push all code to github, thanks