Description
This repository contains a sample service that follows the standard code structure described in
this documentation
- This service implements profile-api V2 protobuf.
- This service contains examples of get, create, update, delete, and a couple more complex cases.
The code is close to the real code that you'll be writing for your API rather than a toy example.
- There's a fully functional
Makefile
to help you run common commands.
The goal of this repository is to show what a full-fledged service looks like,
so that you can see the code structure and patterns.
You are free to remove anything you don't need for your service.
Usage
- Copy all directories and files under this project to your project.
- Global replace
gitlab.com/picnic-app/backend/project-templates/role-api
with your go module name.
- Global replace
role-api
with your project name.
For example, if your project is called messaging-api
, then replace role-api
with messaging-api
.
- Adjust the migrations according to your service design.
- Adjust the configuration files in
.cfg/k8s
and the config
package according to your service requirements.
- Start adding your own DB tables, models, controller methods, service handlers, repositories, and tests
following the same pattern as the sample code.
- Remove sample unused codes.
DB Transaction
- When several repository calls need to be in one DB transaction, the service should start this transaction
using
TxManager
.
- The
TxManager
helps create a transaction and add it to the context.
The repository can then check the context to see if there's a transaction already before it starts a new transaction.
- Use
dbHelper
in the repository to help with identifying transactions.
Example code in the service handler:
if err := h.repo.TxManager().ReadWriteTx(
ctx, func(ctx context.Context) error {
var err error
exists, err := h.repo.ExistsUserByUsername(ctx, user.Username)
if err != nil {
return err
}
if exists {
return errs.Errorf(errs.CodeUsernameAlreadyExists, user.Username)
}
user, err = h.repo.InsertUser(ctx, user)
if err != nil {
return err
}
return nil
},
); err != nil {
return "", err
}
Naming Conventions
Spanner key and index naming
- Index naming rule:
- < Table name >By< Column names >
- Example: UsersByUsername index.
- FK rule:
- FK_< FK column name without
Id
>< Current table name without s
>
- Example:
FK_FollowedUserFollow
is a FK in the Follows
table for the FollowedUserId
column
which references the Id
column of the Users
table.
Model naming
- Each DB table has a corresponding model. The model name is the table name without
s
suffix.
For example, table Users
has model User
Service naming
- Service methods should be named according to business context, for example:
- GetUser
- CreateUser (but not InsertUser)
- BanUser (but not UpdateUserBan)
- Follow Google API standard method naming
as well as their custom method naming.
- A service method that handles a controller method should have the same name as the controller method.
This helps with searching and consistency.
For example, the controller method is
UpdateMe
, and the service handler method is called UpdateMe
as well.
func (c Controller) UpdateMe(ctx context.Context, req *profileV2.UpdateMeRequest) (*profileV2.UpdateMeResponse, error) {
return &profileV2.UpdateMeResponse{}, c.service.UpdateMe(
ctx,
model.UserUpdate{}.FromUpdateMeRequest(req),
)
}
Repository naming
A repository method name should describe a CRUD operation with the DB.
-
For SELECT, method name starts with Get
, for example:
- GetUserBy< Column >: Select one user, e.g. GetUserByUsername
- GetUsersBy< Column >: Select many users, e.g. GetUsersByUsername
- GetUsersBy< Column >With< Filter >: Select many users matching some criteria, e.g.
GetUsersByFollowedUserIDWithFilter
- GetForSomething: A special select statement for a specific purpose that can’t fit in above cases.
-
For SELECT EXISTS, method name starts with Exists
, for example:
- ExistsUserBy< Column >: Select Exists, e.g. ExistsUserByUsername
-
For UPDATE, method name starts with Update
, for example:
- UpdateUser: Update one user
- UpdateUsers: Update many users
-
For INSERT, method name starts with Insert
, for example:
- InsertUser: Insert one user
- InsertUsers: Insert many users
-
For DELETE, method name starts with Delete
, for example:
- DeleteUserBy< Column >: Delete one user, e.g. DeleteUserByID
- DeleteUsersBy< Column >: Delete many users
Useful Commands
-
To install all dependencies
make deps
-
To build app
make build
-
To format code:
make format
-
To run linter:
make lint
-
To run unit tests:
make test/unit
-
To run end-to-end tests using the Spanner emulator:
make test/e2e
-
To run all tests:
make test/all
-
To start a local Spanner Emulator and run all DB migrations in this instance:
make emulator/create
-
To destroy the local Spanner Emulator instance:
make emulator/destroy
How to...
How to start development locally?
Please refer to this notion page.
How to make GRPC calls?
grpcurl -plaintext -v -d '{"user_id" : "1", "cursor" : { "limit" : 1} }' localhost:8082 user.v1.UserService/GetFollowers
grpcurl -plaintext -v -d '{"user_id" : "6" }' localhost:8082 user.v1.UserService/GetFo
llowedUsers