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

Post hooks

A post hook is your function that runs after an API, with the result in g.res.output. Write it in any of the languages of API Maker. It can reshape the answer, add to it, replace it, send something elsewhere, or refuse to answer. It lives at the same levels as a pre hook : instance, database, table, API, custom and system API, with optional groups.

In short

  • A post hook is your code that runs after an API, with its answer in g.res.output.
  • It can change the answer, add to it, or send something elsewhere.
  • It lives at the same levels as a pre hook : instance, database, table, one API.
Hooks around an API : pre hooks from the instance down to the API, the API, then post hooks from the API up to the instance request Caller with tokens pre hooks · top to bottom Instance every table of it Database every table of it Table every API of it This API get all only… The API database · cache post hooks · bottom to top This API g.res.output Table Database Instance Reply to the caller a pre hook which returns a value, or throws, answers here : nothing after it runs Also hooked Custom APIs System APIs A hook with groups runs only for API users of those groups Cached answers run no hook Calls from your own code (g.sys.db…) skip the hooks unless you pass skipHookRunning: false.
API Hooks: Run Your Code Before and After Every API11 chapters and the transcript

Order

Post hooks run from the narrowest level to the widest : the hooks of the API, then of the table, then of the database, then of the instance, each list top to bottom. The last one to run sees what every other one did.

In API Testing, the Post Hooks tab of an API of a table lists every post hook which runs after it, in that order : its own, then the hooks of its table, its database and its instance (marked Table, Database, Instance : they are changed in their categories of API Testing). In Git, every hook is a folder of its own.

A post hook is written in the language of its API, as every hook is : the code of an API and all its hooks are one language on one runtime.

The code

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

async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return;

    // g.res.output : the answer of the API (data), changeable
    // g.res.statusCode, g.res.warnings, g.res.errors
    // g.req : the request as the API saw it, after the pre hooks
    // g.res.shared : what the pre hooks left for you
}
module.exports = main;
from am import G


async def main(g: G):
    if not g.req.is_api_request_from_user:
        return

    # g.res.output : the answer of the API (data), changeable
    # g.res.status_code, g.res.warnings, g.res.errors
    # g.req : the request as the API saw it, after the pre hooks
    # g.res.shared : what the pre hooks left for you
<?php
function main(AmG $g)
{
    if (!$g->req->isApiRequestFromUser) {
        return null;
    }

    // $g->res->output : the answer of the API (data), changeable
    // $g->res->statusCode, $g->res->warnings, $g->res->errors
    // $g->req : the request as the API saw it, after the pre hooks
    // $g->res->shared : what the pre hooks left for you
    return null;
}
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (!g.Req.IsApiRequestFromUser) return null;

        // g.Res.Output : the answer of the API (data), changeable
        // g.Res.StatusCode, g.Res.Warnings, g.Res.Errors
        // g.Req : the request as the API saw it, after the pre hooks
        // g.Res.Shared : what the pre hooks left for you
        return null;
    }
}
package code

import "apimaker/am"

func Main(g *am.G) (any, error) {
    if !g.Req.IsApiRequestFromUser() {
        return nil, nil
    }

    // g.Res.Output() : the answer of the API (data), changeable with g.Res.SetOutput
    // g.Res.StatusCode(), g.Res.Warnings(), g.Res.Errors()
    // g.Req : the request as the API saw it, after the pre hooks
    // g.Res.Shared() : what the pre hooks left for you
    return nil, nil
}
use am::{Errors, G, Value};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    if !g.req.is_api_request_from_user() {
        return Ok(Value::Null);
    }

    // g.res.output_mut() : the answer of the API (data), changeable
    // g.res.status_code(), g.res.warnings(), g.res.errors()
    // g.req : the request as the API saw it, after the pre hooks
    // g.res.shared() : what the pre hooks left for you
    Ok(Value::Null)
}
import am.G;

class Api {
    static Object main(G g) {
        if (!g.req.isApiRequestFromUser()) return null;

        // g.res.getOutput() : the answer of the API (data), changeable with g.res.setOutput
        // g.res.getStatusCode(), g.res.getWarnings(), g.res.getErrors()
        // g.req : the request as the API saw it, after the pre hooks
        // g.res.getShared() : what the pre hooks left for you
        return null;
    }
}
import am.G

fun main(g: G): Any? {
    if (!g.req.isApiRequestFromUser) return null

    // g.res.output : the answer of the API (data), changeable
    // g.res.statusCode, g.res.warnings, g.res.errors
    // g.req : the request as the API saw it, after the pre hooks
    // g.res.shared : what the pre hooks left for you
    return null
}
The hook Effect
returns nothing The next post hook runs, then the answer is sent.
changes g.res.output The answer is what is in g.res.output at the end.
returns a value That value becomes the answer and the remaining post hooks do not run.
throws The request answers with the error, whatever the API did. The database change of the API is not rolled back by that.
  • A hook in any language other than TypeScript runs in the sandbox of its language. A hook in TypeScript runs inside API Maker when it needs nothing of a sandbox, else in its sandbox of Node.js or Bun ; the answer is the same. (runOnNativeProcess runs a hook on the machine of the server.) Every hook runs within the sandbox timeout of the call.
  • No hook runs when the answer comes from the cache. Calls from your own code skip hooks unless skipHookRunning: false.

Reshape the answer

Hide a field and add a computed one
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return;
    const rows = Array.isArray(g.res.output) ? g.res.output : [ g.res.output ];
    for (const row of rows) {
        if (!row) continue;
        delete row.password;
        row.amount_with_tax = Math.round(row.amount * 1.18);
    }
}
module.exports = main;
Hide a field and add a computed one
from am import G


async def main(g: G):
    if not g.req.is_api_request_from_user:
        return
    rows = g.res.output if isinstance(g.res.output, list) else [g.res.output]
    for row in rows:
        if not row:
            continue
        row.pop('password', None)
        row['amount_with_tax'] = round((row.get('amount') or 0) * 1.18)
Hide a field and add a computed one
<?php
function main(AmG $g)
{
    if (!$g->req->isApiRequestFromUser) {
        return null;
    }
    $output = $g->res->output;
    $rows = is_array($output) && array_is_list($output) ? $output : [$output];
    foreach ($rows as $row) {
        if (!$row) {
            continue;
        }
        unset($row->password);
        $row->amount_with_tax = round(($row->amount ?? 0) * 1.18);
    }
    return null;
}
Hide a field and add a computed one
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (!g.Req.IsApiRequestFromUser) return null;
        IEnumerable<JsonObject> rows = g.Res.Output switch
        {
            JsonArray list => list.OfType<JsonObject>(),
            JsonObject row => [row],
            _ => [],
        };
        foreach (var row in rows)
        {
            row.Remove("password");
            row["amount_with_tax"] = Math.Round(row.GetDouble("amount") * 1.18);
        }
        return null;
    }
}
Hide a field and add a computed one
package code

import (
    "math"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    if !g.Req.IsApiRequestFromUser() {
        return nil, nil
    }
    rows, isList := g.Res.Output().([]any)
    if !isList {
        rows = []any{g.Res.Output()}
    }
    for _, each := range rows {
        row := am.AsM(each)
        if row == nil {
            continue
        }
        delete(row, "password")
        row["amount_with_tax"] = math.Round(row.Float("amount") * 1.18)
    }
    return nil, nil
}
Hide a field and add a computed one
use am::{Errors, G, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    if !g.req.is_api_request_from_user() {
        return Ok(Value::Null);
    }
    let output = g.res.output_mut();
    let rows: Vec<&mut Value> = match output {
        Value::Array(rows) => rows.iter_mut().collect(),
        row => vec![row],
    };
    for row in rows {
        let Some(row) = row.as_object_mut() else { continue };
        row.remove("password");
        let amount = row.get("amount").and_then(Value::as_f64).unwrap_or(0.0);
        row.insert("amount_with_tax".to_owned(), json!((amount * 1.18).round()));
    }
    Ok(Value::Null)
}
Hide a field and add a computed one
import am.G;
import java.util.List;
import java.util.Map;

class Api {
    static Object main(G g) {
        if (!g.req.isApiRequestFromUser()) return null;
        Object output = g.res.getOutput();
        List<?> rows = output instanceof List<?> list ? list : List.of(output);
        for (Object each : rows) {
            if (!(each instanceof Map<?, ?> row)) continue;
            @SuppressWarnings("unchecked")
            Map<String, Object> columns = (Map<String, Object>) row;
            columns.remove("password");
            Object amount = columns.get("amount");
            columns.put("amount_with_tax", Math.round((amount instanceof Number number ? number.doubleValue() : 0) * 1.18));
        }
        return null;
    }
}
Hide a field and add a computed one
import am.G

fun main(g: G): Any? {
    if (!g.req.isApiRequestFromUser) return null
    val rows = g.res.output as? List<*> ?: listOf(g.res.output)
    for (each in rows) {
        @Suppress("UNCHECKED_CAST")
        val row = each as? MutableMap<String, Any?> ?: continue
        row.remove("password")
        row["amount_with_tax"] = Math.round(((row["amount"] as? Number)?.toDouble() ?: 0.0) * 1.18)
    }
    return null
}

Do something after a write

Audit row and an event after a save
import * as T from 'types';

async function main(g: T.IAMGlobal) {
    if (!g.req.isApiRequestFromUser) return;
    const now = Date.now();   // milliseconds since 1970
    await g.sys.db.saveSingleOrMultiple({
        instance: 'mongodb', database: 'shop', collection: 'audit_logs',
        saveData: { api: g.req.reqInfo.apiInfo.id, by: g.req.auth.authAMDB?.email, at: now, took: now - g.res.shared.startedAt },
    });
    await g.sys.system.emitEvent('order-placed', g.res.output);
}
module.exports = main;
Audit row and an event after a save
import time

from am import G


async def main(g: G):
    if not g.req.is_api_request_from_user:
        return
    now = int(time.time() * 1000)  # milliseconds since 1970
    await g.sys.db.save_single_or_multiple(
        'mongodb', 'shop', 'audit_logs',
        save_data={
            'api': g.req.req_info.apiInfo.id,
            'by': (g.req.auth.authAMDB or {}).get('email'),
            'at': now,
            'took': now - g.res.shared.startedAt,
        },
    )
    await g.sys.system.emit_event('order-placed', g.res.output)
Audit row and an event after a save
<?php
function main(AmG $g)
{
    if (!$g->req->isApiRequestFromUser) {
        return null;
    }
    $now = (int) (microtime(true) * 1000); // milliseconds since 1970
    $g->sys->db->saveSingleOrMultiple('mongodb', 'shop', 'audit_logs', saveData: [
        'api' => $g->req->reqInfo->apiInfo->id,
        'by' => $g->req->auth->authAMDB?->email,
        'at' => $now,
        'took' => $now - $g->res->shared->startedAt,
    ]);
    $g->sys->system->emitEvent('order-placed', $g->res->output);
    return null;
}
Audit row and an event after a save
file static class Api
{
    public static async Task<object?> Main(G g)
    {
        if (!g.Req.IsApiRequestFromUser) return null;
        var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // milliseconds since 1970
        await g.Sys.Db.SaveSingleOrMultiple(new Save("mongodb", "shop", "audit_logs", new
        {
            api = g.Req.ReqInfo.GetObject("apiInfo").GetString("id"),
            by = g.Req.Auth.GetObject("authAMDB").GetString("email"),
            at = now,
            took = now - g.Res.Shared.GetInt64("startedAt"),
        }));
        await g.Sys.System.EmitEvent("order-placed", g.Res.Output?.DeepClone());
        return null;
    }
}
Audit row and an event after a save
package code

import (
    "time"

    "apimaker/am"
)

func Main(g *am.G) (any, error) {
    if !g.Req.IsApiRequestFromUser() {
        return nil, nil
    }
    now := time.Now().UnixMilli() // milliseconds since 1970
    _, err := g.Sys.Db.SaveSingleOrMultiple(am.Save{
        Instance: "mongodb", Database: "shop", Collection: "audit_logs",
        SaveData: am.M{
            "api":  g.Req.ReqInfo().Map("apiInfo").String("id"),
            "by":   g.Req.Auth().Map("authAMDB").Get("email"),
            "at":   now,
            "took": now - g.Res.Shared().Int("startedAt"),
        },
    })
    if err != nil {
        return nil, err
    }
    _, err = g.Sys.System.EmitEvent("order-placed", g.Res.Output())
    return nil, err
}
Audit row and an event after a save
use std::time::{SystemTime, UNIX_EPOCH};

use am::{Errors, G, Save, Value, json};

pub fn main(g: &mut G) -> Result<Value, Errors> {
    if !g.req.is_api_request_from_user() {
        return Ok(Value::Null);
    }
    let now = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |since| since.as_millis() as i64); // milliseconds since 1970
    g.sys.db.save_single_or_multiple(Save::new(
        "mongodb",
        "shop",
        "audit_logs",
        json!({
            "api": g.req.req_info()["apiInfo"]["id"],
            "by": g.req.auth()["authAMDB"]["email"],
            "at": now,
            "took": now - g.res.shared()["startedAt"].as_i64().unwrap_or(now),
        }),
    ))?;
    g.sys.system.emit_event("order-placed", g.res.output().clone(), &[])?;
    Ok(Value::Null)
}
Audit row and an event after a save
import am.G;
import am.JsonObject;
import am.Save;

class Api {
    static Object main(G g) {
        if (!g.req.isApiRequestFromUser()) return null;
        long now = System.currentTimeMillis(); // milliseconds since 1970
        g.sys.db.saveSingleOrMultiple(new Save("mongodb", "shop", "audit_logs", JsonObject.of(
            "api", g.req.getReqInfo().getObject("apiInfo").getString("id"),
            "by", g.req.getAuth().getObject("authAMDB").get("email"),
            "at", now,
            "took", now - g.res.getShared().getLong("startedAt"))));
        g.sys.system.emitEvent("order-placed", g.res.getOutput());
        return null;
    }
}
Audit row and an event after a save
import am.G
import am.Save

fun main(g: G): Any? {
    if (!g.req.isApiRequestFromUser) return null
    val now = System.currentTimeMillis() // milliseconds since 1970
    g.sys.db.saveSingleOrMultiple(
        Save(
            "mongodb", "shop", "audit_logs",
            mapOf(
                "api" to g.req.reqInfo.getObject("apiInfo").getString("id"),
                "by" to g.req.auth.getObject("authAMDB")["email"],
                "at" to now,
                "took" to now - g.res.shared.getLong("startedAt"),
            ),
        ),
    )
    g.sys.system.emitEvent("order-placed", g.res.output)
    return null
}
  • For news to WebSocket clients, write nothing : the clients subscribed to the API get its answer after each successful call. Emit a WebSocket event yourself with g.sys.system.emitEventWS when the news needs another shape.
  • For an event after an API hit without code, attach the event to the API on the Events page (automatic trigger).

Streams

Post hooks do not run on get all by stream and query by stream : the rows are on their way while they are read. To reshape a stream, read it in a custom API with g.sys.db.getAllByStream.

Good to know

  • WebSocket news and the events of the API go out after the post hooks, with the final answer.
  • A level holds a list of post hooks, each one active or not. Each has a Test button on the API testing page, like pre hooks, and they deploy with Git.