Skip to content
This page View Markdown Open in ChatGPT Open in Claude

Go

Code in Go defines func Main(g *am.G) (any, error) in a package which imports apimaker/am. It is built once as a plugin of the runner, on the official image of Go (golang:1.27 unless the settings name another version), with only the modules you add.

In short

  • Write func Main(g *am.G) (any, error) in a package that imports apimaker/am.
  • It is built once, on golang:1.27 unless the settings name another version.
  • Refuse a request with return nil, am.Fail(400, message).
hello.go
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    name := g.Req.Body().String("name")
    if name == "" {
        name = "world"
    }
    return am.M{"hello": name}, nil
}
Runtime Go 1.21 or later, a version of golang:<version>
Main func Main(g *am.G) (any, error) : what it returns is the answer, an error refuses
Calls of g.Sys typed : what a call takes is a struct (am.GetAll{…}), what it answers has its type ([]am.M, int, am.UpdateManyResult)
Values am.M (map[string]any) for objects and rows, []any for lists, float64 for numbers
Package any name, package code in a new item : the sandbox builds it as a plugin whatever it is named. In git every piece of code is a package of its own, in a folder of its own
Packages Go modules : github.com/google/uuid v1.6.0
Printing fmt.Println and log go to the console of the account

The request and the answer

g.Req and g.Res have a method for each key they are known to have : g.Req.Body(), g.Req.Query(), g.Req.Params(), g.Req.Headers(), g.Res.SetStatusCode(201). What they give is an am.M : Map, List, String, Float, Int and Bool read a key without a type assertion and give the zero value when it is not there. The map is the one inside the request, so changing it changes the request.

A body which is a list ([{…}, {…}], several rows to save) is read with g.Req.List("body") ; g.Req.Body() is an empty am.M then.

Read the request, shape the answer
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    status := g.Req.Query().String("status")
    if status == "" {
        status = "open"
    }
    _, signed := g.Req.Headers()["x-am-authorization"]
    g.Res.SetStatusCode(201)
    g.Res.Headers()["X-Request-Kind"] = "orders"
    return am.M{"customer": g.Req.Params().String("id"), "status": status, "signed": signed}, nil
}

Bytes as the answer

A file, a picture, a PDF : return its bytes (a []byte), or give them to g.Res.SetOutput, and name their kind of content. The answer is those bytes as they are, whatever the kind of content, with the headers of the code. (A text under a kind of bytes, image/gif, is read as base64.)

A picture as the answer
package code

import (
    "encoding/base64"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    picture, err := base64.StdEncoding.DecodeString("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=")
    if err != nil {
        return nil, err
    }
    g.Res.SetContentType("image/gif")
    g.Res.Headers()["Content-Disposition"] = `inline; filename="pixel.gif"`
    return picture, nil
}

Calling API Maker : g.Sys

Every call has its types. What it takes is a struct of am whose fields are its keys (am.GetAll, am.GetById, am.Count…) ; what it answers is typed too : rows are a []am.M, a row an am.M (nil when there is none), a count an int, a key of the cache a *string (nil when it is not there), what an API describes a struct (am.UpdateManyResult).

Database calls
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    rows, err := g.Sys.Db.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers", Limit: 10, Sort: "-customer_id"})
    if err != nil {
        return nil, err
    }
    two, err := g.Sys.Db.GetById(am.GetById{Instance: "mysql_8", Database: "inventory", Collection: "customers", Id: 2})
    if err != nil {
        return nil, err
    }
    count, err := g.Sys.Db.Count(am.Count{Instance: "mysql_8", Database: "inventory", Collection: "customers", Find: am.M{"country": "IN"}})
    if err != nil {
        return nil, err
    }
    var first am.M
    if len(rows) > 0 {
        first = rows[0]
    }
    return am.M{"first": first, "name": two.String("first_name"), "inIndia": count}, nil
}
  • A key which is left out is not sent. A flag which is on by default is a pointer, so false can be said : NoCycle: am.Ptr(false).
  • A key the struct has no field for goes in Other : am.GetAll{Instance: "mysql_8", Other: am.M{"cursor": "abc"}}.
  • A row as a struct of your own : customer, err := am.To[Customer](rows[0]).
What a call answers has its type
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    changed, err := g.Sys.Db.UpdateMany(am.UpdateMany{Instance: "mysql_8", Database: "inventory", Collection: "customers",
        Find: am.M{"country": "IN"}, UpdateData: am.M{"active": true}})
    if err != nil {
        return nil, err
    }
    // What a save answers : saved.Row (one row was saved), saved.Rows (a list was), saved.IsList
    saved, err := g.Sys.Db.SaveSingleOrMultiple(am.Save{Instance: "mysql_8", Database: "inventory", Collection: "customers",
        SaveData: am.M{"first_name": "Ada"}})
    if err != nil {
        return nil, err
    }
    // The whole answer of a call : whole.Success, whole.StatusCode, whole.Data ([]am.M), whole.TotalCount, whole.Errors
    whole, err := g.Sys.Db.Full.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers", GetTotalCount: true})
    if err != nil {
        return nil, err
    }
    return am.M{"updated": changed.UpdatedRowsCount, "id": saved.Row["customer_id"], "of": whole.TotalCount, "rows": len(whole.Data)}, nil
}

Calls at the same time are goroutines :

Calls at the same time
package code

import (
    "sync"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    counts := map[string]any{}
    var lock sync.Mutex
    var wait sync.WaitGroup
    for _, collection := range []string{"orders", "products"} {
        wait.Add(1)
        go func(collection string) {
            defer wait.Done()
            count, _ := g.Sys.Db.Count(am.Count{Instance: "mongodb", Database: "shop", Collection: collection})
            lock.Lock()
            counts[collection] = count
            lock.Unlock()
        }(collection)
    }
    wait.Wait()
    return counts, nil
}

Rows as they arrive

Sum a large table
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    total := 0.0
    err := g.Sys.Db.GetAllByStream(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "orders"}, func(row am.M) error {
        total += row.Float("amount")
        return nil
    })
    return am.M{"amount": total}, err
}

System APIs and cache

Encrypt, hash, events and cache keys
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    sealed, err := g.Sys.System.Encrypt(am.M{"card": "4111"})
    if err != nil {
        return nil, err
    }
    digest, _ := g.Sys.System.Hash("my text")
    _, _ = g.Sys.System.EmitEvent("order_created", am.M{"id": 7}, "notify_shop")
    _, _ = g.Sys.Cache.SetKey("last_order", "7", 600)
    last, _ := g.Sys.Cache.GetKey("last_order")
    return am.M{"sealed": sealed, "digest": digest, "last": last}, nil
}

Errors

Return am.Fail(code, message) to refuse, or an *am.Errors with several errors :

Refuse a request
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    if g.Req.Body().Get("quantity") == nil {
        return nil, am.Fail(400, "'quantity' is missing.")
    }
    return am.M{"ok": true}, nil
}

A call which fails gives an *am.Errors with the errors of the server :

Catch the errors of a call
package code

import (
    "errors"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    _, err := g.Sys.Db.RemoveById(am.RemoveById{Instance: "mysql_8", Database: "inventory", Collection: "customers", Id: 404})
    var refused *am.Errors
    if errors.As(err, &refused) {
        g.Logger.Warn("not removed :", refused.Error())
        return am.M{"removed": false, "code": refused.Code()}, nil
    }
    return am.M{"removed": err == nil}, err
}
  • Another error answers 500 with its message. A panic answers 500 with panic: … and the lines of your code (at orders.go:14 in helper()).
  • A panic in a goroutine you started ends the runner : recover inside your goroutines.

Logs, printing and shared values

Logs and shared values
package code

import (
    "fmt"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    g.Logger.Log("order", 7, "paid")           // "order 7 paid"
    g.Logger.Error(am.M{"step": "payment"})     // maps, slices and structs as indented JSON
    fmt.Println("kept in the console of the account")
    // Lives as long as the runner : Update changes a value while no other request does.
    calls := g.Shared.Update("calls", func(old any) any {
        count, _ := old.(int)
        return count + 1
    })
    return calls, nil
}

Hooks

Pre hook : only my rows
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    person := g.Req.Auth().Map("authAMDB")
    query := g.Req.Query()
    find := query.Map("find")
    if find == nil {
        find = am.M{}
        query["find"] = find
    }
    find["owner_id"] = person.Get("id")
    return nil, nil
}
Post hook : hide a column
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    for _, row := range g.Res.List("output") {
        delete(am.AsM(row), "password")
    }
    return nil, nil
}

Utility classes

A utility class written in Go is a package. The other Go code of the account imports it by the folder it has in the Git repository, which is the module apimaker :

The utility class Tax, in the folder utils
package tax

import round "apimaker/src/utility_classes/utils---Round" // another utility class

var Rate = 0.2

func Add(amount float64) float64 {
    return round.Cents(amount * (1 + Rate))
}
A custom API which uses it
package code

import (
    "apimaker/am"
    tax "apimaker/src/utility_classes/utils---Tax"
)

func Main(g *am.G) (any, error) {
    return am.M{"total": tax.Add(g.Req.Body().Float("amount"))}, nil
}
  • The folder of a class is src/utility_classes/<folder>---<name> (utils---Tax for the class Tax in utils/) : letters of a to z, digits, ., - and _. The name after package is yours; name the import as you like.
  • The package is built once and shared by all Go code of the account in a sandbox : what it keeps in a variable is kept there. A class which is saved again is another build, which the code gets when it is compiled again on its next run.
  • go build ./... in the repository builds the same imports : nothing else is needed.

Test cases

A test case written in Go lists its tests in Tests, in the place of a Main. A test is a func(g *am.G) error : the error it returns, or what it panics with, is its failure.

A test case
package code

import (
    "fmt"

    "apimaker/am"
)

func theCostOfAParcel(g *am.G) error {
    if err := g.Sys.Test.Mock(am.Mock{Method: "db.getAll", When: am.M{"collection": "rates"}, Return: []any{am.M{"per_kg": 2.5}}}); err != nil {
        return err
    }
    out, err := g.Sys.Test.RunCustomApi(am.RunCustomApi{Name: "shippingCost", Body: am.M{"weight": 4}})
    if err != nil {
        return err
    }
    if cost := am.AsM(out).Float("cost"); cost != 10 {
        return fmt.Errorf("the cost is %v", cost)
    }
    return nil
}

var Tests = []am.Test{
    {Name: "the cost of a parcel", Run: theCostOfAParcel},
}
  • g.Sys.Test : RunCustomApi(am.RunCustomApi{…}), RunCustomApiFull(…) (the whole answer : Data, StatusCode, Headers, Logs), Mock(am.Mock{…}), Mocks([]am.Mock{…}), ClearMocks(method...), Calls(method...).
  • The custom API under test can be written in any language.

Packages

Add github.com/google/uuid in Sandbox Settings, then :

A Go module
package code

import (
    "github.com/google/uuid"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    return am.M{"id": uuid.NewString()}, nil
}
  • A version without v gets one : 1.6.0 is v1.6.0; empty is latest.
  • Modules are downloaded when the image is built; while code runs nothing is downloaded (GOPROXY=off).

How it runs

  • Each piece of code is built once as a plugin of the runner (about a second), from the same module as the runner : the packages of the account and apimaker/am are shared. Saving starts the build in the background; code which does not compile is reported in the console of the account (main.go:6:9: undefined: missing).
  • Every request is a goroutine; requests run at the same time, on four threads of Go at most. Code of an API waits for its database most of the time, and Go wakes an idle thread for every goroutine which becomes ready : with a thread for every core of a big machine that costs more than the request. Code which computes on many cores sets GOMAXPROCS to the number it wants, with an ENV line of the Dockerfile of Go : ENV GOMAXPROCS=16.
  • Variables at package level live as long as the runner : protect them with a mutex, or use g.Shared.

In the code editor

The editor completes g as Go writes it, the names of your instances, databases and tables, and the columns of a table on the rows a call answers. More in the code editor in every language.

Where the editor helps
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    // g.  →  Req, Res, Sys, Logger, Shared      g.Sys.Db.  →  GetAll, GetById, Query, Gen, Full…
    g.Logger.Log(g.Req.Body())  // g.Req.  →  Body, Query, Headers, Auth…      g.Res.  →  SetStatusCode, Headers…
    rows, err := g.Sys.Db.GetAll(am.GetAll{Instance: "mysql_8", Database: "inventory", Collection: "customers"})
    //           am.GetAll{  →  Instance, Database, Collection, Find, Limit, Sort…
    //           Instance: "  →  your instances   Collection: "  →  the tables of mysql_8.inventory
    if err != nil {
        return nil, err
    }
    for _, row := range rows {
        g.Logger.Log(row.String("first_name"))  // .String("  →  the columns of customers
    }
    return rows, nil
}
  • am. offers what the package has : the structs of what the calls take (am.GetAll, am.Save…), am.M, am.Fail, am.To. Between the braces of such a struct the editor offers its fields, each with what it is for ; inside Find: am.M{" the columns of the table, inside Select: " and Sort: " too.
  • What a call answers has its type : on the rows of a GetAll the columns are offered inside the quotes of Get, Map, List, String, Float, Int and Bool, and of an index (rows[0][") ; changed. offers UpdatedRowsCount, saved. offers Row, Rows and IsList, whole. (a call of g.Sys.Db.Full) Success, Data, Errors…
  • g.Req. and g.Res. offer a method for each key of the request and of the answer ; g.Req.Auth(). the opened tokens.

Reference

g. Go
Req Body(), Query(), Params(), Headers(), Auth(), ReqInfo() (each an am.M), EventData(), IsApiRequestFromUser(), and Set… for each
Res SetStatusCode, SetContentType, Headers(), Shared(), Output(), SetOutput, Errors(), Warnings()
an am.M Get, Map, List, String, Float, Int, Bool
Sys.Db GetAll(am.GetAll), GetById(am.GetById), Query(am.Query), SaveSingleOrMultiple(am.Save), MasterSave(am.Save), ArrayOperations(am.ArrayOperations), UpdateById(am.UpdateById), UpdateMany(am.UpdateMany), ReplaceById(am.UpdateById), RemoveById(am.RemoveById), RemoveByQuery(am.RemoveByQuery), Aggregate(am.Aggregate), Count(am.Count), Distinct(am.Distinct), DistinctQuery(am.Distinct), GetAllByStream, QueryByStream; Sys.Db.Gen.GetAllGen…
Sys.Db.Full, Sys.Db.Gen.Full, Sys.System.Full, Sys.Cache.Full the same calls, each with its whole answer : am.ApiResponse[T] (Success, StatusCode, Data, TotalCount, Errors, Warnings)
Sys.System Encrypt, EncryptWith, Decrypt, DecryptWith, Hash, GetSecret, GetSecrets, CallExternalApi, CallExternalApis, GetToken, GetTokens, ExecuteQuery, GetTableMeta, GetTableMetas, CreateIndexes, DropIndexes, DropIndexesOfMany, GetIndexes, GetIndexesOfMany, EmitEvent, EmitEventWS, IsValidDataForTable(s), IsValidDataForCustomAPI(s), MultiTenantInstance(s)Updated, IsValidConnectionString(s), GetLock, ReleaseLock
Sys.Cache GetKey, GetKeys, SetKey, SetKeys, RemoveKey, RemoveKeys, ResetCacheDB, ResetCacheDBs, ResetCacheCustomApis, ResetCacheCustomApisOfTenant, ResetCacheSystemApis, ResetCacheSystemApisOfTenant
Sys.Call("db.getAll", …) any API by its name in JavaScript, with its arguments as they are
Logger Log, Debug, Info, Warn, Error
Shared Get, Set, Update
errors am.Fail(code, message), &am.Errors{List: []am.M{…}}

Debugging

  • The debug runner of Go builds your code without optimizations and runs under Delve : the Debug tab of the API testing page steps through it.
  • A panic stops the code when Pause on exceptions is on.
  • Code on the native process of a Mac needs the developer mode of the Mac (sudo DevToolsSecurity -enable) : Delve works through the debugserver of Xcode.
  • Learn more Debugging.