• Start

Custom APIs

Managing APIs

Defining custom HTTP-style endpoints on SurrealDB so clients hit a narrow surface instead of arbitrary queries.

Custom APIs let you expose a small, deliberate set of routes on top of SurrealDB. Instead of handing every caller a generic query channel, you can define named endpoints that return a familiar HTTP-shaped result.

That pattern pairs well with tightening access such as varying querying timeouts for free or entry-level users on an app. To accomplish this, you can combine DEFINE API with capabilities or server environment variables so certain users only see the APIs you designed.

Each definition maps to a path under /api/:namespace/:database/..., followed by the path you gave in the statement. As such, an endpoint path like get_users in namespace my_namespace and database my_database becomes something like /api/my_namespace/my_database/get_users over HTTP.

Inside the handler you can use the built-in $request value. This parameter holds the method, body, headers, query, params (from your path pattern), and context you or middleware may have set.

Here is a small endpoint that echoes part of the body and sets a couple of headers. The precise clauses and permissions are spelled out in the DEFINE API reference page.

Defining an API endpoint

DEFINE API "/test"
FOR get, post
MIDDLEWARE
api::timeout(1s)
THEN {
{
status: 200,
body: {
request: $request.body,
response: "The server works"
},
headers: {
'last-modified': <string>time::now(),
'expires': <string>(time::now() + 4d)
}
};
};

You can test the endpoint from SurrealQL without needing to deploy by calling the api::invoke function. This function takes either the path alone or the path plus a body object.

api::invoke("/test");

api::invoke("/test", {
body: {
hi: "please",
give: "me",
the: "information"
}
});

The path string in DEFINE API can be static, or it can capture pieces of the URL.

A segment like "/users/:id" binds one path component—anything in that slot shows up on $request.params. A trailing pattern with * instead of : matches everything from that point—handy for nested paths or file-like routes.

DEFINE API OVERWRITE "/test/:anything_goes" FOR get THEN {
RETURN {
body: {
some: "data"
}
}
};

api::invoke("/test/this_matches");
api::invoke("/test/same_here");
api::invoke("/test/but/this/wont/match");

Here the first two calls hit the handler; the third does not, because :anything_goes only covers a single segment—extra slashes mean “no matching route”, which surfaces as a 404-style result from api::invoke.

To accept multiple trailing segments, switch the capture to the * form:

DEFINE API OVERWRITE "/test/*anything_goes" FOR get THEN {
RETURN {
body: {
some: "data"
}
}
};

api::invoke("/test/this_matches");
api::invoke("/test/same_here");
api::invoke("/test/works/with/multiple/paths/now");

All three calls succeed because the remainder of the path is treated as one captured piece.

Built-in helpers such as api::timeout sit in the MIDDLEWARE list before your THEN block. For more details on how to use middleware, see the next page.

Was this page helpful?