Codebase
The API for this project is created mainly using Go. Some middlewares is created using Node.js with Express.js frameworks.
API
Get Started
The API is built using Go. The API is divided into services. Each services consist of few files. First being main.go as the entrypoint of the service API. To differ the main and routes, each service has a package called routes
. This package is used to declare the routes of the service. This route need to follow the endpoint format of:
So, it's mandatory to use a subrouter for ease of use. Below is a snippet on how to declare the subrouter: 1. Create a subrouter with the version in main.go before setting the route
// Preview the library section for more info on
// the wrapper routerX
r := router.CreateRouterx()
// This will create a router with /v1/ prefix
routes.SetServiceRoute(r.PathPrefix("/v1").Subrouter())
func SetServiceRoute(r *mux.Router) {
// This will create a router with prev_prefix/service/
// prefix
subrouter := r.PathPrefix("/service").Subrouter()
/* Set route handler /*
}
Besides routes and the entrypoint, each service has it's own config.go files that are placed on config package. This files declared the configuration of the server, such as host, database, and other service that this service depend on. config.go
will parse configuration from json files called app.config.json
that are placed within the same folder as the config.go files.
Below is an example on how to create config.go files for api service
package config
type Config struct {
ServiceName string `json:"service_name"`
/*Other Fields*/
}
func ParseConfig(path string) {
// Read the other configuration from app.config.json
exe, err := os.Executable()
if err != nil {
panic(err)
}
exePath := filepath.Dir(exe)
configFile, err := os.Open(filepath.Join(exePath, "", path))
if err != nil {
return nil, err
}
defer configFile.Close()
err = jsonutil.DecodeJSON(configFile, &config)
if err != nil {
return nil, err
}
/* Do other things */
return config, err
}
Warning
For ease of deployment, any secret key or other sensitive configuration, place them inside an environment variables. REFRAIN FROM PLACING THEM ON THE CONFIG FILE.
Custom Library
This codebase use a custom http library which are an extensions of Golang http library.
Custom Handler Type
In this library, there are few types that are declared, that is
This type is the http.Handler
equivalent, and used to create a handler for a route. This struct implement the http.Handler
interface.
Metadata
This handler two field, that is, metadata, which is a struct type
This struct hold data/struct that may be needed on the handler, such as database and cache, and configuration files.
HandlerLogic
As for HandlerLogic
, it's a function that act as the main logic of the handler. The function has a signature as follows:
type HandlerLogic func(metadata *Metadata, w http.ResponseWriter, r *http.Request) responseerror.HTTPCustomError
Custom HTTP Error
For convenience, each handler must return a custom error type called HTTPCustomError
.
The field is straightforward. Code is the return code of the error, such as 404 for Not Found, 400 for Bad Request, etc. Name is the name of the error, which must be an ENUM. When creating a new name of the error, you must add them into the list of all error type in the correct error group (Bad Request, Not Found, etc.).
For message, it's a brief description of the errors that were thrown. Same as errorType, you must declare the message on the correct group as a template. This makes the message dynamic. You can add {{.dynamicFields}}
into your string. responseerror
package has a function named ParseMessage
that can replace the placeholder with the value on a map with the same key.
type errorMessageTemplate string
const (
template errorMessageTemplate "Hello, {{.name}}. {{.MOTD}}"
)
s := responseerror.ParseMessage(tmp errorMessageTemplate, map[string]string{
"name": "John Smith",
"MOTD": "Good Morning!",
})
fmt.Println(s) // "Hello, John Smith. Good Morning!"
All Errors
-
Bad Request
const ( MissingParameter errorType = "missing_parameter" HeaderValueMistmatch errorType = "header_value_mismatch" UsernameExists errorType = "username_exists" EmailExists errorType = "email_exists" UsernameInvalid errorType = "username_invalid" PasswordWeak errorType = "password_weak" EmailInvalid errorType = "invalid_email" PayloadInvalid errorType = "invalid_payload" OTPInvalid errorType = "invalid_otp" OTPExpired errorType = "otp_expired" CallsStatusInvalid errorType = "invalid_status_value" SteamNotLinked errorType = "steam_not_linked" SteamAlreadyLinked errorType = "steam_has_been_linked" MalformedSessionID errorType = "malformed_uuidv7_id" InvalidPIN errorType = "invalid_pin" ) const ( MissingParameterMessage errorMessageTemplate = "required field {{.field}} is missing" HeaderValueMistmatchMessage errorMessageTemplate = "mismatch value in header {{.name}}" UsernameExistMessage errorMessageTemplate = "email already taken" EmailsExistMessage errorMessageTemplate = "username already taken" UsernameInvalidMessage errorMessageTemplate = "username is invalid" PasswordWeakMessage errorMessageTemplate = "password is weak" EmailInvalidMessage errorMessageTemplate = "email is invalid" PayloadInvalidMessage errorMessageTemplate = "payload is invalid" OTPInvalidMessage errorMessageTemplate = "otp is invalid" OTPExpiredMessage errorMessageTemplate = "otp is expired. please resend a new otp to your email" StatusInvalidMessage errorMessageTemplate = "trying to update status to {{.reqStatus}} when user status is {{.status}}" SteamAlreadyLinkedMessage errorMessageTemplate = "this account has been linked to steam" SteamNotLinkedMessage errorMessageTemplate = "account has not been linked to steam yet" MalformedSessionIDMessage errorMessageTemplate = "{{.id}} cannot be parsed as a valid uuidv7 id " InvalidPINMessage errorMessageTemplate = "{{.pin}} is invalid" )
-
Unauthenticated
const ( UserMarkedInActive errorType = "user_marked_inactive" InvalidCredentials errorType = "invalid_credentials" ) const ( UserMarkedInActiveMessage errorMessageTemplate = "your account hasn't been verified and currently marked inactive" InvalidCredentialsMessage errorMessageTemplate = "email or password is wrong" )
-
Unauthorized
const ( InvalidAuthHeader errorType = "invalid_auth_header" EmptyAuthHeader errorType = "empty_auth_header" InvalidToken errorType = "invalid_token" TokenExpired errorType = "token_expired" RefreshDenied errorType = "refresh_denied" ClaimsMismatch errorType = "claims_mismatch" AccessDenied errorType = "access_denied" ) const ( InvalidAuthHeaderMessage errorMessageTemplate = "Not accepted authorization of type {{.authType}}" EmptyAuthHeaderMessage errorMessageTemplate = "Required authorization header in request header" InvalidTokenMessage errorMessageTemplate = "Invalid Token.{{.description}}" TokenExpiredMessage errorMessageTemplate = "your token has expired" RefreshDeniedMessage errorMessageTemplate = "cannot get new access token when the previous one still active" ClaimsMismatchMessage errorMessageTemplate = "refresh claims and username claims don't share the same credentials" MTLSFailureMessage errorMessageTemplate = "cannot establish trust. certificate is either invalid, revoked, or empty" AccessDeniedMesage errorMessageTemplate = "you don't have permission to access this route" )
-
Internal Service Error
Info
Internal Service Error is a unique one. It add a new field to the struct. That is the description. When a Internal Service Error is thrown, the service will print the error description
.
- Too Many Request
Querynator
This API don't use ORM library to make the query. Querynator
is the library that were used to create query to the database. Querynator
is built on PostgreSQL on mind, so the functionality may break when using other driver.
Querynator has three mode, normal, batch insert, and join.
As for now, querynator support this operation: 1. Insert (Normal Mode) 2. Batch Insert (Filter Batch Insert Mode 3. Filter Batch Insert (Filter Batch Insert Mode) 4. Update (Normal Mode) 5. Delete (Normal Mode) 6. Find (Normal Mode) 7. FindOne (Normal Mode) 8. IsExist (Normal Mode) 9. Find (Join Mode) 10. Transaction (Begin, Commit, and Rollback)
Querynator
use sqlx
for some advanced functionality such as parse multiple SQL rows into a struct. This is used heavily on Find.
Other than that, it used plain SQL interface from the standard library.
Normal Mode
1. Insert
func (q *Querynator) Insert(v interface{}, db QueryOperation, tableName string, returnField string) (interface{}, error)
tableName
. v must be of type struct
and has db
tag on the fields to specify the column name of the fields. If returnField
is not an empty string, interface{}
will has the return value from column specified in returnField
Important
Insert will ignore field that aren't initialized (equal to the zero value for basic type and length is zero for a slice)
-
Delete
This function will delete v from the database from table equal totableName
. -
Update
This function will update data in the database with non empty value from v. Data that fit the condition specified will be updated. -
IsExists
Check if v exist in the database -
FindOne
Find one row that has the same value as v and load the result to dest.
- Find Find all occurences in the database that has the same value as v and load them into dest
Join
To change mode to Join Mode, call the initiate join operation methods of querynator.
This will create aJoinQueryExecutor
instance.
1. Add Join Table
This will attach a table into the join operation
func (e *JoinQueryExecutor) AddJoinTable(joinTableName string, joinKeyName string, receiverTableName string, receiverForeignKeyName string) {
joinTableName
is the new table that are going to be added to the operation. receiverTableName
is the name of the table that are matched against the new table, and must have already been declared/added to the operation (either via AddJoinTable
or the name that are placed on the Find method)
2. UseExplicitCast
const (
ASCENDING OrderType = "ASC"
DESCENDING OrderType = "DESC"
)
func (e *JoinQueryExecutor) OrderBy(orderByField string, orderType OrderType) {
func (e *JoinQueryExecutor) Find(db *sql.DB, condition []QueryCondition, dest interface{}, tableName string, joinClause JoinClause, returnFields map[string][]string) error {
returnFields
that matched the conditions specified. If the returnFields
is happened to have a null value, the query will change the value into a zero value of the appropriate type.
Filter Operation
To create filter operation, one must first call PrepareFilterOperation
methods of querynator
.
This will return a FilteredQueryExecutor
.
Filter query is an operation where the operation is conducted by looking at other table values. If the data fit the criteria, the operations is executed, else it was ignored.
Info
Filter query currently only support batch insert.
-
AddTableSource
This will add table that will be used for comparison for the data -
UseTransaction
This will begin a transcation. The transaction instance will be placed on field called Tx.
- BatchInsert
Insert data in batch. 4. Rollback
Rollback the operation. 6. Commit
Commit the operation.
Routerx and Routex
This is an extensions to mux.Router
To create a new routerx
, call the function CreateRouterx
Routerx
only implements a handful of mux.Router
methods, that includes, Handle
, PathPrefix
, and Use
. Other functionality can be called via Router
field.
Routerx
also not implement http.Handler
interface, so to attach router to http server, use Router
field.
Equivalent to mux.Router
. Handle methods attach handler into a route instance called routex
, that are a wrapper to mux.Route
.
routex
, add one new methods, that is UseMiddleware
. This will attach a middleware chain to the handler of the route.
Middleware
Middleware is a http.Handler
that can called other http.Handler
specified in next
. In this codebase, middleware is declared as a type
This signature follows the mux.Middlewarefnc
type.
A few useful middleware has been implemented, that is 1. Payload Check Middleware 2. Auth Middleware 3. Certificate Check Middleware 4. Route Getter Middleware
Middleware 1-3 need to be initialized. Middleware 4 does not.
Here is a snippet on auth middleware factory
func AuthMiddleware(authAPI ServiceAPI, tlsConfig *tls.Config, metadata *httpx.Metadata) Middleware {
return func(next http.Handler) http.Handler {
fn := func(metadata *httpx.Metadata, w http.ResponseWriter, r *http.Request) responseerror.HTTPCustomError {
var token string
endpoint := r.Context().Value(EndpointKey).(string)
auth := r.Header.Get("Authorization")
if auth == "" {
return responseerror.CreateBadRequestError(
responseerror.EmptyAuthHeader,
responseerror.EmptyAuthHeaderMessage,
nil,
)
}
if authType, authValue, _ := strings.Cut(auth, " "); authType != "Bearer" {
return responseerror.CreateUnauthorizedError(
responseerror.InvalidAuthHeader,
responseerror.InvalidAuthHeaderMessage,
map[string]string{
"authType": authType,
},
)
} else {
token = authValue
}
req := &httpx.HTTPRequest{}
req, err := req.CreateRequest(
authAPI.Scheme,
authAPI.Host,
authAPI.Port,
AUTH_VERIFY_TOKEN_ENDPOINT,
http.MethodPost,
http.StatusOK,
struct {
Token string `json:"token"`
Endpoint string `json:"endpoint"`
}{
Token: token,
Endpoint: endpoint,
},
tlsConfig,
)
if err != nil {
return err
}
claims := &jwtutil.Claims{}
err = req.Send(claims)
if err != nil {
return err
}
// send token and claims into the next middleware chain
ctx := context.WithValue(r.Context(), ClaimsKey, claims)
ctx = context.WithValue(ctx, TokenKey, token)
next.ServeHTTP(w, r.WithContext(ctx))
return nil
}
return &httpx.Handler{
Metadata: metadata,
Handler: fn,
}
}
}
Other middleware follows a similar pattern.
Warning
Auth middleware required you to use route getter middleware before this middleware. Omitting that middleware will create a panic. Beware!
Info
data is being passed through middleware chain via context. Use the ContextKey to retrieve any relevant information.