> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# APIs, functions and middleware

> Publish endpoints under an API group, share logic between endpoints and tools with functions, run work in the background, and attach middleware such as a rate limit.

An endpoint is a `query()` def object published under an `apiGroup()`. Its public path is
`/api:<canonical>/<name>`, and its `stack` is the logic that runs per request. Logic that more
than one caller needs lives in a function, and steps that wrap many objects live in middleware.

## An API group and its endpoints

```ts theme={null}
// xano/api/blog.ts
import { apiGroup, query, input, inp, ref, auth, s, c, expr } from "@xano/sdk";
import { posts, users } from "../tables/blog.js";

export const blog = apiGroup({ name: "blog", canonical: "blog" });

// GET /api:blog/posts/{id}: one row is addressed in the path.
export const getPost = query({
  name: "posts/{id}",
  verb: "GET",
  apiGroup: blog,
  input: { id: input.int({ required: true }) },
  stack: [
    s.db.get({ table: posts, fieldValue: inp("id"), as: "post" }),
    s.precondition({
      expr: expr(ref("post"), "!=", c.null()),
      error_type: "notfound",
      error: c.text("No such post."),
    }),
  ],
  response: ref("post"),
});

// POST /api:blog/posts: only a signed-in user from the users table can call it.
export const createPost = query({
  name: "posts",
  verb: "POST",
  apiGroup: blog,
  auth: users,
  input: {
    title: input.text({ required: true }),
    body: input.text(),
  },
  stack: [
    s.db.add({
      table: posts,
      row: { title: inp("title"), body: inp("body"), author_id: auth("id") },
      as: "post",
    }),
  ],
  response: ref("post"),
});
```

Register them in `xano/index.ts` with `.registerApiGroups([blog])` and
`.registerQueries([getPost, createPost])`.

* **Path params.** Each `{param}` segment in `name` needs an input of the same name, or `query()`
  throws. Read it with `inp()` like any other input. Inputs not in the path are query-string
  params on a GET and body fields otherwise.
* **Identity.** An endpoint is identified by its group, verb and name, so `GET posts` and
  `POST posts` are two endpoints. Changing the verb is an identity change, not an edit.
* **Errors with a status.** `s.precondition` fails the request with the status its `error_type`
  names: `badrequest` 400, `unauthorized` 401, `accessdenied` 403, `notfound` 404,
  `toomanyrequests` 429. `s.throw` answers 200 with an error body, so a client cannot detect it
  from the status.
* **Auth.** `auth: users` names an auth table (`table({ auth: true })`). The caller sends a
  bearer token for a row in it, and `auth("id")` reads that row's id. For signup and login
  endpoints, install the [auth add-on](/build/add-ons) or follow the email and password pattern
  in the [authoring reference](/reference/authoring).
* **CORS.** `apiGroup({ cors: { mode: "custom", allowOrigins: [...], allowMethods: {...} } })`
  answers browsers on known origins. `mode: "default"` serves a permissive policy.

## Functions

A `defineFunction()` is logic other code calls. Only a `query` is reachable by verb and path, and
only a `tool` is callable by an agent or an MCP client, so logic that serves an endpoint and a
tool lives in one function both of them run with `s.function.run`:

```ts theme={null}
// xano/functions/posts.ts
import { defineFunction, input, inp, ref, s, c, col, expr, and } from "@xano/sdk";
import { posts } from "../tables/blog.js";

export const postsByAuthor = defineFunction({
  name: "posts_by_author",
  input: { author_id: input.int({ required: true }) },
  stack: [
    s.db.query({
      table: posts,
      where: and(
        expr(col("author_id"), "=", inp("author_id")),
        expr(col("published"), "=", c.bool(true)),
      ),
      paging: { per_page: 20, metadata: false },
      as: "rows",
    }),
  ],
  response: ref("rows"),
});
```

```ts theme={null}
import { query, tool, input, inp, ref, s } from "@xano/sdk";
import { postsByAuthor } from "../functions/posts.js";

// GET /api:blog/authors/{id}/posts
export const authorPosts = query({
  name: "authors/{id}/posts",
  verb: "GET",
  apiGroup: blog,
  input: { id: input.int({ required: true }) },
  stack: [s.function.run({ fn: postsByAuthor, input: { author_id: inp("id") }, as: "rows" })],
  response: ref("rows"),
});

// The same logic for an agent or an MCP client.
export const authorPostsTool = tool({
  name: "author_posts",
  description: "List an author's published posts.",
  input: { author_id: input.int({ required: true }) },
  stack: [s.function.run({ fn: postsByAuthor, input: { author_id: inp("author_id") }, as: "rows" })],
  response: ref("rows"),
});
```

Register them with `.registerFunctions([postsByAuthor])`, `.registerQueries([authorPosts])`
and `.registerTools([authorPostsTool])`.

* **`input` is keyed by the function's input names.** An undeclared name is a type error.
  Pass every input explicitly: one you omit is filled from the caller's own input of the same
  name, if it has one.
* **The result is typed.** `as` binds the function's response type, so `ref("rows")` carries
  the row type into the endpoint's response.
* **Functions take tests.** Give one `tests` and it is asserted on its own. See
  [Tests](/build/tests).

## Work a request does not wait for

Add `runtime: { mode: "async-shared" }` to `s.function.run` and the call moves off the request
path. The endpoint answers straight away and the function runs on the instance's async
workers:

```ts theme={null}
import { defineFunction, query, input, inp, ref, s } from "@xano/sdk";
import { users } from "../tables/blog.js";

export const sendWelcome = defineFunction({
  name: "send_welcome",
  input: { user_id: input.int({ required: true }) },
  stack: [
    s.db.get({ table: users, fieldValue: inp("user_id"), as: "user" }),
    // ...call the mail provider with s.api.request
    s.debug.log({ value: ref("user") }),
  ],
  response: ref("user"),
});

// POST /api:blog/welcome: answers at once; the email goes out in the background.
export const welcome = query({
  name: "welcome",
  verb: "POST",
  apiGroup: blog,
  input: { user_id: input.int({ required: true }) },
  stack: [
    s.function.run({
      fn: sendWelcome,
      input: { user_id: inp("user_id") },
      runtime: { mode: "async-shared" },
      as: "job", // the job id (a UUID string), not the function's result
    }),
  ],
  response: { job: ref("job") },
});
```

An async call does not return the function's result. Its `as` binds a job id, and `s.await`
collects results by id. That also runs independent work side by side:

```ts theme={null}
import { query, input, inp, ref, s, c, withFilters, fl } from "@xano/sdk";
import { postsByAuthor } from "../functions/posts.js";

export const compareAuthors = query({
  name: "authors/compare",
  verb: "GET",
  apiGroup: blog,
  input: { a: input.int({ required: true }), b: input.int({ required: true }) },
  stack: [
    s.function.run({ fn: postsByAuthor, input: { author_id: inp("a") }, runtime: { mode: "async-shared" }, as: "job_a" }),
    s.function.run({ fn: postsByAuthor, input: { author_id: inp("b") }, runtime: { mode: "async-shared" }, as: "job_b" }),
    s.await({
      ids: withFilters(c.array([]), fl.array_push(ref("job_a")), fl.array_push(ref("job_b"))),
      timeout: c.int(10), // seconds
      as: "results", // { "<job id>": result, ... }
    }),
  ],
  response: ref("results"),
});
```

* **A timeout returns nothing.** A run still going when `timeout` passes, or an unknown id,
  fails the request with a 500 `Async timeout`. There is no partial result.
* **`"async-dedicated"`** reserves its own resources: it reads `cpu`, `memory`, `timeout` and
  `maxRetry` from the same `runtime` block. `s.ai.agent.run` takes the same `runtime`.
* **Work on a timer** is a [task](/build/tasks), and work that follows a row change is a
  [table trigger](/build/data-and-tables#react-to-row-changes-table-triggers).

## Middleware

A `middleware()` is a reusable stack that runs before (`pre`) or after (`post`) its host: an auth
check, a rate limit or logging that many objects share, instead of the same steps repeated at
the top of each stack. You attach it to a query, a function, a task, a tool or an API group, or
to every host of a kind at once with `workspaceConfig({ middleware })`. This one rate-limits a
public endpoint by client IP:

```ts theme={null}
// xano/api/rate-limit.ts
import { middleware, s, c, sys, withFilters, fl } from "@xano/sdk";

export const publicRateLimit = middleware({
  name: "public_rate_limit",
  exceptionPolicy: "rethrow", // a tripped limit aborts the request with a 429
  stack: [
    s.redis.ratelimit({
      key: withFilters(c.text("rl:public:"), fl.concat(sys.remoteIp())),
      max: c.int(20),
      ttl: c.int(60),
      error: c.text("Too many requests. Try again shortly."),
    }),
  ],
});
```

Attach it with the host's `middleware` field, and register it with
`.registerMiddleware([publicRateLimit])`:

```ts theme={null}
export const getPost = query({
  name: "posts/{id}",
  verb: "GET",
  apiGroup: blog,
  middleware: { pre: [publicRateLimit] },
  // ...input, stack and response as above
});
```

To attach it to every endpoint at once, set the workspace tier and register the config with
`.registerWorkspace(config)`:

```ts theme={null}
// xano/workspace.ts
import { workspaceConfig } from "@xano/sdk";
import { publicRateLimit } from "./api/rate-limit.js";

export const config = workspaceConfig({
  middleware: {
    query: { pre: [publicRateLimit] }, // every endpoint without a closer override
  },
});
```

The map takes `query`, `function`, `task` and `tool` keys. Setting it replaces the whole
workspace map, so a host kind you leave out has its workspace middleware cleared. Omit
`middleware` to leave what the workspace holds untouched.

What to know before you write your own:

* **`exceptionPolicy`** decides whether a guard guards. The SDK writes `"rethrow"` by default,
  which aborts the request. `"silent"` swallows the error and lets the request through, so use it
  only for advisory middleware.
* **Inheritance is override.** A phase you set on an endpoint replaces the API group's chain for
  that phase, and the group's replaces the workspace's. `pre: middleware.clear()` sets a phase to
  nothing.
* **Key a public endpoint by IP.** On an endpoint with no `auth`, `auth("id")` does not resolve
  and the request fails with a 403. Use `sys.remoteIp()` there, as above.
* **One middleware object is one counter.** Attaching the same rate limit to several endpoints
  shares its key, so `max` becomes a budget across all of them. Vary the key prefix for a
  separate limit per endpoint.
* **Redis is an instance capability.** `s.redis.*` statements fail at request time on an
  instance without cache support.

## Next

* Webhooks, request history, `resultStrategy` and every statement:
  [authoring reference](/reference/authoring).
* Call these endpoints from a frontend with types: [Typed frontend](/build/typed-frontend).
* Assert they work: [Tests](/build/tests).
* Run a stack on a schedule: [Tasks](/build/tasks).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.