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

Custom APIs

A custom API is your own function behind a path and a method. Write it in any of the languages of API Maker. Save it in the admin panel (APIs & Logic → Custom API), or in your editor through the local client. It answers at /api/custom-api/<user-path>/<path> at once. Your code reaches your databases and the rest of API Maker through the global object g.

In short

  • Two files : the basic info (path, method, access, cache…) in TypeScript, and the code in your language.
  • main gets g. What it returns is data in the answer.
  • The code runs in a sandbox of your account, or on the server with runOnNativeProcess: true.

New to your own code ?

Start with Your own code : one custom API, step by step, on the real screens.

URL /api/custom-api/<user-path>/<path of the settings>
Method the requestMethod of the settings : GET, POST, PUT or DELETE
Body, query, params, headers, files g.req.body, g.req.query, g.req.params, g.req.headers, g.req.body.files
Answer what main returns, in data of the envelope, or a file, or any content type
Language TypeScript on Node.js or Bun, Python, PHP, C#, Go, Rust, Java or Kotlin : picked above the editor, codeLanguage of the API
Runs in the sandbox of the account for its language ; the machine of the server with runOnNativeProcess: true
Access apiAccessType : TOKEN_ACCESS (default), IS_PUBLIC, NO_ACCESS (only from other code)
From code g.sys.system.callExternalApi over HTTP, or g.sys.test.runCustomApi in a test
Your First Custom API: From a Function to a Live Endpoint12 chapters and the transcript

Hello world

Every custom API has two files : the basic info (its settings) and the code. The code is written in any language ; the basic info is always TypeScript.

Basic info
import * as T from 'types';

let customApi: T.ICustomApiSettingsTypes = {
    name: 'Hello World',
    path: '/hello-world',
    requestMethod: T.ERequestMethod.GET,
    apiAccessType: T.EAPIAccessType.TOKEN_ACCESS,
    errorList: [],
};
module.exports = customApi;
Code
1
2
3
4
5
6
7
import * as T from 'types';
import * as db from 'db-interfaces';

async function main(g: T.IAMGlobal) {
    return { hello: g.req.query.name || 'world' };
}
module.exports = main;
Code
1
2
3
4
5
from am import G


async def main(g: G):
    return {'hello': g.req.query.name or 'world'}
Code
1
2
3
4
5
<?php
function main(AmG $g)
{
    return ['hello' => $g->req->query->name ?? 'world'];
}
Code
1
2
3
4
5
6
7
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        return new { hello = g.Req.Query.GetString("name", "world") };
    }
}
Code
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    name := g.Req.Query().String("name")
    if name == "" {
        name = "world"
    }
    return am.M{"hello": name}, nil
}
Code
1
2
3
4
5
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    Ok(json!({ "hello": g.req.query()["name"].as_str().unwrap_or("world") }))
}
Code
1
2
3
4
5
6
7
8
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        return Map.of("hello", g.req.getQuery().getString("name", "world"));
    }
}
Code
1
2
3
4
5
import am.G

fun main(g: G): Any? {
    return mapOf("hello" to g.req.query.getString("name", "world"))
}
curl "$AM/api/custom-api/admin/hello-world?name=Bob" -H "x-am-authorization: $TOKEN"
{ "success": true, "statusCode": 200, "data": { "hello": "Bob" } }

Pick the language above the editor, and the editor offers its template. The list of the custom APIs shows the language of each with its logo. The hooks of a custom API are written in the language of its code.

  • The path and the method must match exactly : /hello-world with GET. Variables go in the query string or the body, not in the path.
  • path + requestMethod is unique in the account ; name is unique too and names the folder in Git.

The settings

Key Meaning
name, path, requestMethod Identity and URL.
apiAccessType TOKEN_ACCESS : the API user token, plus the person token when authProviders names one. IS_PUBLIC : no token. NO_ACCESS : not reachable over HTTP, only from other code and the testing page.
authProviders Names of the auth providers whose token the person must send. Absent : the providers of the default secret. [] : the API user token only.
enableCaching Cache the answer in Redis, per URL, body and headers that matter, until resetCacheOnModificationOf says otherwise or the reset custom API cache system API runs.
resetCacheOnModificationOf What resets the cache : 'DB:instance:database:table' when that table is written through API Maker, 'CA:name' when that custom API is called.
acceptOnlyEncryptedData Refuse plain bodies : the body must be an encrypted payload.
reqBodySchema, reqQueryParametersSchema A schema for the body and for the query params : converted and validated before the code runs, every error at once with 400.
errorList The messages your API throws, listed so they can be translated with i18n and shown in the docs of the API.
runOnNativeProcess Run on the machine of the server instead of in the sandbox : the packages of the Sandbox Settings, the packages of the folder API Maker runs in and the programs of the machine, no isolation. Use with care.
customApiTimeoutInSeconds How long the code and the hooks of the API may run, in seconds, wherever they run (the sandbox, runOnNativeProcess). Empty : sandboxReqTimeout of the configuration (13 seconds). A request which sends x-am-sandbox-timeout is given that time instead.
separateSandboxSettings A sandbox of its own for this API, with its own runtime, version, packages and count : separate sandbox.
fileUpload enable, allowFileUploadFields, and per field minFileSizeBytes, maxFileSizeBytes, allowedExtensionsArr.
swaggerDocs What the Swagger document of the API users shows for this API.

See Custom API settings for every key with its example.

Validate the request with a schema

Basic info
let customApi: T.ICustomApiSettingsTypes = {
    name: 'Create Order', path: '/orders', requestMethod: T.ERequestMethod.POST, errorList: [],
    reqBodySchema: {
        customer_id: { __type: T.EType.number, validations: { required: true } },
        items: [ { product_id: { __type: T.EType.string, validations: { required: true } }, qty: { __type: T.EType.number, validations: { min: 1 } } } ],
    },
    reqQueryParametersSchema: {
        dryRun: { __type: T.EType.boolean, conversions: { defaults: { defaultValue: false } } },
    },
};
  • The body and the query params reach your code converted (types, trims, defaults) and checked (required, min, enum…). A wrong request never reaches the code.
  • The is valid data for custom API system API runs the same check without calling the API.

Reach your data and the rest of API Maker

import * as T from 'types';
import * as db from 'db-interfaces';

async function main(g: T.IAMGlobal) {
    const person = g.req.auth.authAMDB;                          // the signed-in person
    const orders = await g.sys.db.query<db.mongodb.shop.IOrders>({
        instance: 'mongodb', database: 'shop', collection: 'orders',
        find: { customer_id: person.customer_id, status: 'PAID' },
        sort: '-created_at', limit: 10,
        deep: [ { s_key: 'items' } ],
    });
    const total = orders.reduce((sum, order) => sum + order.amount, 0);
    g.logger.log(`${orders.length} orders`);                     // reaches the log table and the caller's logs
    await g.sys.cache.setKey(`orders:${person.customer_id}`, JSON.stringify(orders), 60);
    return { orders, total };
}
module.exports = main;
import json

from am import DeepItem, G


async def main(g: G):
    person = g.req.auth.authAMDB                                 # the signed-in person
    orders = await g.sys.db.query(
        'mongodb', 'shop', 'orders',
        find={'customer_id': person.customer_id, 'status': 'PAID'},
        sort='-created_at', limit=10,
        deep=[DeepItem('items')],
    )
    total = sum(order.amount or 0 for order in orders)
    g.logger.log(f'{len(orders)} orders')                       # reaches the log table and the caller's logs
    await g.sys.cache.set_key(f'orders:{person.customer_id}', json.dumps(orders), 60)
    return {'orders': orders, 'total': total}
<?php
function main(AmG $g)
{
    $person = $g->req->auth->authAMDB;                           // the signed-in person
    $orders = $g->sys->db->query(
        'mongodb', 'shop', 'orders',
        find: ['customer_id' => $person->customer_id, 'status' => 'PAID'],
        sort: '-created_at', limit: 10,
        deep: [new DeepItem('items')],
    );
    $total = array_sum(array_map(fn ($order) => $order->amount ?? 0, $orders));
    $g->logger->log(count($orders) . ' orders');                 // reaches the log table and the caller's logs
    $g->sys->cache->setKey("orders:{$person->customer_id}", json_encode($orders), 60);
    return ['orders' => $orders, 'total' => $total];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        var person = g.Req.Auth.GetObject("authAMDB");                  // the signed-in person
        var orders = await g.Sys.Db.Query(new Query("mongodb", "shop", "orders")
        {
            Find = new JsonObject { ["customer_id"] = person["customer_id"]?.DeepClone(), ["status"] = "PAID" },
            Sort = "-created_at", Limit = 10,
            Deep = new[] { new DeepItem("items") },
        });
        var total = orders.Sum(order => order.GetDouble("amount"));
        g.Logger.Log($"{orders.Count} orders");                          // reaches the log table and the caller's logs
        await g.Sys.Cache.SetKey($"orders:{person["customer_id"]}", JsonSerializer.Serialize(orders), 60);
        return new { orders, total };
    }
}
package code

import (
    "encoding/json"
    "fmt"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    person := g.Req.Auth().Map("authAMDB") // the signed-in person
    orders, err := g.Sys.Db.Query(am.Query{
        Instance: "mongodb", Database: "shop", Collection: "orders",
        Find: am.M{"customer_id": person.Get("customer_id"), "status": "PAID"},
        Sort: "-created_at", Limit: 10,
        Deep: []am.DeepItem{am.DeepItem{SKey: "items"}},
    })
    if err != nil {
        return nil, err
    }
    total := 0.0
    for _, order := range orders {
        total += order.Float("amount")
    }
    g.Logger.Log(fmt.Sprintf("%d orders", len(orders))) // reaches the log table and the caller's logs
    kept, err := json.Marshal(orders)
    if err != nil {
        return nil, err
    }
    if _, err := g.Sys.Cache.SetKey(fmt.Sprintf("orders:%v", person.Get("customer_id")), string(kept), 60); err != nil {
        return nil, err
    }
    return am.M{"orders": orders, "total": total}, nil
}
use am::{DeepItem, Errors, G, Query, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let person = g.req.auth()["authAMDB"].clone(); // the signed-in person
    let orders = g.sys.db.query(
        Query::new("mongodb", "shop", "orders")
            .find(json!({ "customer_id": person["customer_id"], "status": "PAID" }))
            .sort("-created_at").limit(10)
            .deep(vec![DeepItem::new("items")]),
    )?;
    let total: f64 = orders.iter().map(|order| order["amount"].as_f64().unwrap_or(0.0)).sum();
    g.logger.log(format!("{} orders", orders.len())); // reaches the log table and the caller's logs
    g.sys.cache.set_key(&format!("orders:{}", person["customer_id"]), &json!(orders).to_string(), Some(60))?;
    Ok(json!({ "orders": orders, "total": total }))
}
import am.DeepItem;
import am.G;
import am.Json;
import am.JsonObject;
import am.Query;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        JsonObject person = g.req.getAuth().getObject("authAMDB");              // the signed-in person
        List<JsonObject> orders = g.sys.db.query(new Query("mongodb", "shop", "orders")
            .find(Map.of("customer_id", person.get("customer_id"), "status", "PAID"))
            .sort("-created_at").limit(10)
            .deep(List.of(new DeepItem("items"))));
        double total = orders.stream().mapToDouble(order -> order.getDouble("amount")).sum();
        g.logger.log(orders.size() + " orders");                                // reaches the log table and the caller's logs
        g.sys.cache.setKey("orders:" + person.get("customer_id"), Json.stringify(orders), 60);
        return Map.of("orders", orders, "total", total);
    }
}
import am.DeepItem
import am.G
import am.Json
import am.Query

fun main(g: G): Any? {
    val person = g.req.auth.getObject("authAMDB")                               // the signed-in person
    val orders = g.sys.db.query(
        Query("mongodb", "shop", "orders")
            .find(mapOf("customer_id" to person["customer_id"], "status" to "PAID"))
            .sort("-created_at").limit(10)
            .deep(listOf(DeepItem("items"))),
    )
    val total = orders.sumOf { it.getDouble("amount") }
    g.logger.log("${orders.size} orders")                                        // reaches the log table and the caller's logs
    g.sys.cache.setKey("orders:${person["customer_id"]}", Json.stringify(orders), 60)
    return mapOf("orders" to orders, "total" to total)
}
  • In TypeScript await every g.sys call (a coroutine of Python and a task of C# too) ; the other languages wait for the answer of each call. The whole envelope instead of an error comes with true as the second argument in TypeScript, and through full in the other languages (g.sys.db.full.query).
  • In TypeScript, import * as db from 'db-interfaces' gives the interfaces generated from your schemas, db.<instance>.<database>.I<Table> ; the editors of the other languages offer the same columns on the rows a call answers.
  • Code shared by many APIs (a price, a rule) goes into a utility class.
  • Every method of g has a page in the code examples.

Errors and status codes

import * as T from 'types';

async function main(g: T.IAMGlobal) {
    if (!g.req.body?.customer_id) {
        throw [{ code: T.EStatusCode.BAD_REQUEST, message: 'Please provide customer_id.' }];   // 400 with the message in errors
    }
    if (g.req.body.customer_id < 0) {
        g.res.statusCode = T.EStatusCode.BAD_REQUEST;                                       // 400 with your data
        return { field: 'customer_id', message: 'A customer_id is positive.' };
    }
    return { ok: true };
}
module.exports = main;
from am import Errors, G


async def main(g: G):
    if not g.req.body.customer_id:
        raise Errors('Please provide customer_id.', code=400)    # 400 with the message in errors
    if g.req.body.customer_id < 0:
        g.res.status_code = 400                                    # 400 with your data
        return {'field': 'customer_id', 'message': 'A customer_id is positive.'}
    return {'ok': True}
<?php
function main(AmG $g)
{
    if (empty($g->req->body->customer_id)) {
        throw new AmErrors('Please provide customer_id.', 400);   // 400 with the message in errors
    }
    if ($g->req->body->customer_id < 0) {
        $g->res->statusCode = 400;                                 // 400 with your data
        return ['field' => 'customer_id', 'message' => 'A customer_id is positive.'];
    }
    return ['ok' => true];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (g.Req.Body["customer_id"] is null)
            throw new AmErrors("Please provide customer_id.", 400);   // 400 with the message in errors
        if (g.Req.Body.GetDouble("customer_id") < 0)
        {
            g.Res.StatusCode = 400;                                    // 400 with your data
            return new { field = "customer_id", message = "A customer_id is positive." };
        }
        return new { ok = true };
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    body := g.Req.Body()
    if body.Get("customer_id") == nil {
        return nil, am.Fail(400, "Please provide customer_id.") // 400 with the message in errors
    }
    if body.Float("customer_id") < 0 {
        g.Res.SetStatusCode(400) // 400 with your data
        return am.M{"field": "customer_id", "message": "A customer_id is positive."}, nil
    }
    return am.M{"ok": true}, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    let Some(customer_id) = g.req.body()["customer_id"].as_f64() else {
        return Err(am::fail(400, "Please provide customer_id.")); // 400 with the message in errors
    };
    if customer_id < 0.0 {
        g.res.set_status_code(400); // 400 with your data
        return Ok(json!({ "field": "customer_id", "message": "A customer_id is positive." }));
    }
    Ok(json!({ "ok": true }))
}
import am.AmErrors;
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        if (!g.req.getBody().has("customer_id")) throw new AmErrors("Please provide customer_id.", 400);   // 400 with the message in errors
        if (g.req.getBody().getDouble("customer_id") < 0) {
            g.res.setStatusCode(400);                                                                       // 400 with your data
            return Map.of("field", "customer_id", "message", "A customer_id is positive.");
        }
        return Map.of("ok", true);
    }
}
import am.AmErrors
import am.G

fun main(g: G): Any? {
    if (!g.req.body.has("customer_id")) throw AmErrors("Please provide customer_id.", 400)   // 400 with the message in errors
    if (g.req.body.getDouble("customer_id") < 0) {
        g.res.statusCode = 400                                                                // 400 with your data
        return mapOf("field" to "customer_id", "message" to "A customer_id is positive.")
    }
    return mapOf("ok" to true)
}
  • An error your code raises answers success: false, with its message in errors : throw [{ code, message }] in TypeScript, raise Errors(message, code=400) in Python, AmErrors in PHP, C#, Java and Kotlin, am.Fail(400, message) in Go, am::fail(400, message) in Rust.
  • The code of the first error is the status of the answer when it is 400, 401, 403, 404 or 500. Any other code (422…) answers 500, and the error keeps its code. For another status, set g.res.statusCode.
  • Any other exception answers 500 with its message. A message listed in errorList can be translated with i18n.
  • g.res.statusCode sets the status of a normal answer ; g.res.warnings adds warnings.

Files

Upload. Send multipart/form-data with the files in the field files (or files1, files2… up to files50, to keep groups of files apart). They are on disk when the code runs, in g.req.body.files :

1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    const files: { originalname: string; mimetype: string; size: number; path: string; filename: string }[] = g.req.body.files || [];
    return files.map(f => ({ name: f.originalname, bytes: f.size }));
}
module.exports = main;
1
2
3
4
5
6
from am import G


async def main(g: G):
    files = g.req.body.files or []   # each : originalname, mimetype, size, path, filename
    return [{'name': f.originalname, 'bytes': f.size} for f in files]
1
2
3
4
5
6
7
8
9
<?php
function main(AmG $g)
{
    $answer = [];
    foreach ($g->req->body->files ?? [] as $f) {   // each : originalname, mimetype, size, path, filename
        $answer[] = ['name' => $f->originalname, 'bytes' => $f->size];
    }
    return $answer;
}
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // each : originalname, mimetype, size, path, filename
        return g.Req.Body.GetObjects("files").Select(f => new { name = f.GetString("originalname"), bytes = f.GetInt64("size") }).ToList();
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    answer := []am.M{}
    for _, each := range g.Req.Body().List("files") { // each : originalname, mimetype, size, path, filename
        f := am.AsM(each)
        answer = append(answer, am.M{"name": f.String("originalname"), "bytes": f.Int("size")})
    }
    return answer, nil
}
1
2
3
4
5
6
7
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // each : originalname, mimetype, size, path, filename
    let files = g.req.body()["files"].as_array().cloned().unwrap_or_default();
    Ok(json!(files.iter().map(|f| json!({ "name": f["originalname"], "bytes": f["size"] })).collect::<Vec<_>>()))
}
import am.G;
import am.JsonObject;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        // each : originalname, mimetype, size, path, filename
        List<JsonObject> files = g.req.getBody().getObjects("files");
        return files.stream().map(f -> Map.of("name", f.getString("originalname", ""), "bytes", f.getLong("size"))).toList();
    }
}
1
2
3
4
5
6
import am.G

fun main(g: G): Any? {
    // each : originalname, mimetype, size, path, filename
    return g.req.body.getObjects("files").map { mapOf("name" to it.getString("originalname"), "bytes" to it.getLong("size")) }
}
  • path is where the code finds the file wherever it runs : /opt/am/work/uploads/<filename> in the sandbox, the uploads folder of the server with runOnNativeProcess. uploads/<filename> from the folder the code works in is the same file.
  • A form may be as long as uploadLimit (512 MiB by default) : its files go to disk while they arrive, never into memory. Its text fields are in g.req.body, together no longer than bodyLimit.
  • fileUpload.validations.files.maxFileSizeBytes and allowedExtensionsArr in the settings refuse a wrong file before the code runs. Uploaded files are cleaned from the uploads folder after uploadedFileRemoveOlderThanThisTimeInSeconds (30 minutes by default). A request refused before the code runs (no such API, a caller who may not call it) leaves no file.

Download. Return an object with the __am__ keys of IDownloadResponse : the file (or a zip of several) is streamed to the caller.

import * as T from 'types';

async function main(g: T.IAMGlobal) {
    // a file written earlier in the uploads folder of the sandbox
    return <T.IDownloadResponse>{
        __am__downloadFilePath: 'reports/2026-09.pdf',
        __am__downloadFolderFileName: 'September report.pdf',
        __am__cleanupFileOrFolderPaths: 'reports/2026-09.pdf',
    };
}
module.exports = main;
from am import G


async def main(g: G):
    # a file written earlier in the uploads folder of the sandbox
    return {
        '__am__downloadFilePath': 'reports/2026-09.pdf',
        '__am__downloadFolderFileName': 'September report.pdf',
        '__am__cleanupFileOrFolderPaths': 'reports/2026-09.pdf',
    }
<?php
function main(AmG $g)
{
    // a file written earlier in the uploads folder of the sandbox
    return [
        '__am__downloadFilePath' => 'reports/2026-09.pdf',
        '__am__downloadFolderFileName' => 'September report.pdf',
        '__am__cleanupFileOrFolderPaths' => 'reports/2026-09.pdf',
    ];
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        // a file written earlier in the uploads folder of the sandbox
        return new JsonObject
        {
            ["__am__downloadFilePath"] = "reports/2026-09.pdf",
            ["__am__downloadFolderFileName"] = "September report.pdf",
            ["__am__cleanupFileOrFolderPaths"] = "reports/2026-09.pdf",
        };
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    // a file written earlier in the uploads folder of the sandbox
    return am.M{
        "__am__downloadFilePath":         "reports/2026-09.pdf",
        "__am__downloadFolderFileName":   "September report.pdf",
        "__am__cleanupFileOrFolderPaths": "reports/2026-09.pdf",
    }, nil
}
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    // a file written earlier in the uploads folder of the sandbox
    Ok(json!({
        "__am__downloadFilePath": "reports/2026-09.pdf",
        "__am__downloadFolderFileName": "September report.pdf",
        "__am__cleanupFileOrFolderPaths": "reports/2026-09.pdf",
    }))
}
import am.G;
import java.util.Map;

class Api {
    static Object main(G g) {
        // a file written earlier in the uploads folder of the sandbox
        return Map.of(
            "__am__downloadFilePath", "reports/2026-09.pdf",
            "__am__downloadFolderFileName", "September report.pdf",
            "__am__cleanupFileOrFolderPaths", "reports/2026-09.pdf");
    }
}
import am.G

fun main(g: G): Any? {
    // a file written earlier in the uploads folder of the sandbox
    return mapOf(
        "__am__downloadFilePath" to "reports/2026-09.pdf",
        "__am__downloadFolderFileName" to "September report.pdf",
        "__am__cleanupFileOrFolderPaths" to "reports/2026-09.pdf",
    )
}
  • __am__downloadFileOrFolderPaths with several paths, or { fsSource, archiveDestination } objects, answers a zip.

Any content type

Set g.res.contentType and return the content : HTML, text, XML, YAML, or bytes as base64 for any other MIME type.

1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    g.res.contentType = T.EContentType.HTML;
    return '<h1>Hello</h1>';
}
module.exports = main;
1
2
3
4
5
6
from am import G


async def main(g: G):
    g.res.content_type = 'text/html'
    return '<h1>Hello</h1>'
1
2
3
4
5
6
<?php
function main(AmG $g)
{
    $g->res->contentType = 'text/html';
    return '<h1>Hello</h1>';
}
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        g.Res.ContentType = "text/html";
        return "<h1>Hello</h1>";
    }
}
1
2
3
4
5
6
7
8
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    g.Res.SetContentType("text/html")
    return "<h1>Hello</h1>", nil
}
1
2
3
4
5
6
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    g.res.set_content_type("text/html");
    Ok(json!("<h1>Hello</h1>"))
}
1
2
3
4
5
6
7
8
import am.G;

class Api {
    static Object main(G g) {
        g.res.setContentType("text/html");
        return "<h1>Hello</h1>";
    }
}
1
2
3
4
5
6
import am.G

fun main(g: G): Any? {
    g.res.contentType = "text/html"
    return "<h1>Hello</h1>"
}
A tiny gif
1
2
3
4
5
6
7
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    g.res.contentType = 'image/gif' as T.EContentType;   // not in EContentType : the return is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
}
module.exports = main;
A tiny gif
1
2
3
4
5
6
from am import G


async def main(g: G):
    g.res.content_type = 'image/gif'   # another type : what is returned is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA='
A tiny gif
1
2
3
4
5
6
<?php
function main(AmG $g)
{
    $g->res->contentType = 'image/gif';   // another type : what is returned is base64
    return 'R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=';
}
A tiny gif
1
2
3
4
5
6
7
8
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        g.Res.ContentType = "image/gif";   // another type : what is returned is base64
        return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=";
    }
}
A tiny gif
1
2
3
4
5
6
7
8
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    g.Res.SetContentType("image/gif") // another type : what is returned is base64
    return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=", nil
}
A tiny gif
1
2
3
4
5
6
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    g.res.set_content_type("image/gif"); // another type : what is returned is base64
    Ok(json!("R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="))
}
A tiny gif
1
2
3
4
5
6
7
8
import am.G;

class Api {
    static Object main(G g) {
        g.res.setContentType("image/gif");   // another type : what is returned is base64
        return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA=";
    }
}
A tiny gif
1
2
3
4
5
6
import am.G

fun main(g: G): Any? {
    g.res.contentType = "image/gif"   // another type : what is returned is base64
    return "R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOwA="
}

Around the code

The life of a request : app → Caddy → token and groups → pre hooks → the API → post hooks → reply Your app web · mobile · server or an AI agent Caddy HTTPS ends here :443 → :38246 API Maker one process, or a cluster behind Caddy Token & groups API gate · row gate 401 · 403 if not Pre hooks your code check · change · stop The API schema · generated custom · system Post hooks your code reshape · notify Reply JSON · XML YAML · text · file Redis cache answers again in µs Your databases MongoDB · SQL · Oracle The API asks the cache first when caching is on, then the database. Sandboxed : your code runs in Docker containers, apart from API Maker. x-am-authorization 200 · 201 with data, or 400 · 401 · 403 · 404 with every error at once
  • Hooks : pre and post hooks on the custom API itself, see Pre hooks.
  • Events and WebSockets : an event can trigger automatically after a successful call of the API, and WebSocket clients subscribed to the API get its answer.
  • Versions : several versions of the code, one active ; switch back in one click.
  • Docs tab : notes for the people who use the API, shown in the API testing page and the Swagger of the API users.
  • Test cases : g.sys.test.runCustomApi runs the code with mocks and measures its coverage.
  • Git : src/custom_apis/<name>/ holds the item (<name>.yaml), the basic info (<name>.config.ts), the code (<name>.ts, or .py, .go… in another language), its hooks and the docs. Every file of a custom API, and of every other kind of item, with what goes in it : Items as files.
  • Groups : an API user calls a custom API only when a group grants it, see API group permission.

Native process

runOnNativeProcess: true runs the function on the machine of the server instead of in Docker, in one runner of its runtime shared by every account with the same packages : no sandbox start, the packages of the Sandbox Settings of its language (installed on the machine the first time, in a folder of their own), the packages installed in the folder API Maker runs in, and the programs of the machine, such as a browser for Playwright. Code of a group of separate sandboxes runs on the runtime of its group, with the packages of its group. It runs on the version of its language you installed for it in Sandbox settings → Native languages (every server installs it from the publisher of the language, for your account), or on the runtime of the server (node, python3, php…) in a version API Maker runs, which API Maker Launchpad can install too. The code is not isolated from the server, so keep it for the few APIs which need it.

Separate Sandboxes for Heavy APIs: Bun, Sandbox Count and Restarts10 chapters and the transcript

That runner loads API Maker itself, as a sandbox does (g.sys is answered where the code runs) : g.sys.db.getAll(…), a key of the cache, a secret are function calls in the process of the runner, which reads the databases and Redis itself, for the account of each request. Only the answer of the API goes back to the server. This holds in every language ; a release has the libraries it needs, built for its machine. Where none is found for the machine of the server (a server built by hand on a Mac : AM_NATIVE_CORE in the configuration), the server answers those calls itself, as it says in its log.

Samples in every account

A new account comes with custom APIs under /default/… which show the features at work. Open them on the Custom API page, or read each one in your language on Custom API examples :

Sample Shows
Login the token of the app and the token of the person, one message for every failure
Data Isolation only the rows of the person who calls, with inactive row scoping hooks on the instance, the database and the table orders
Deep Populate Orders, Deep Populate Order Items rows with the rows they point to, on one and two levels
Find Join Orders, Find Join Products a filter on a column of another table
Caching Example an answer kept until its tables change
Get Captcha, Login With Captcha a value kept for a while, used once
WebSocket Notify a message to one person over WebSocket
Collection Copy Stream, Database Copy Stream rows read one by one
S3 Upload, S3 Download, S3 List, Image Upload files in S3

On video

Custom APIs on One Standard: Validation, Standard Errors, Fast Fixes10 chapters and the transcript
Custom API Gems: Uploads, Downloads, npm Packages, a Browser, Caching12 chapters and the transcript
One API in 8 Languages: TypeScript, Python, PHP, C#, Go, Rust, Java, Kotlin8 chapters and the transcript