Skip to content

Statements

/

DEFINE

DEFINE API

Available since: v3.0.0

The DEFINE API statements allows a custom endpoint to be created. Each endpoint created by a DEFINE API statement is located at the /api/:namespace/:database/:endpoint_name path. For example, an endpoint for the path get_users for the namespace my_namespace and database my_database will have the path /api/my_namespace/my_database/get_users.

The response is an object with a combination of the following properties:

  • status - A valid HTTP status code.

  • body - Any value.

  • headers - An object of valid header key value pairs. The value of each pair must be a string.

  • context - An object.

A defined API has access to a preset parameter called $request which contains the request sent by the caller of the endpoint. The $request parameter may contain values at the following fields: body, headers, params, method, query, and context.

SurrealQL Syntax
DEFINE API [ OVERWRITE | IF NOT EXISTS ] @endpoint
    [ FOR @HTTP_method, .. ]
    [ MIDDLEWARE @function, .. ]
    [ THEN { @value } ]
    [ PERMISSIONS [ NONE | FULL | @expression ] ]
    [ SECURITY DEFINER | SECURITY INVOKER ]
    [ COMMENT @string ]

DEFINE API is often used in conjunction with a capabilities flag or environment variable to disable arbitrary queries, thereby forcing record and anonymous users to interact with the database via API endpoints alone.

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)
                }
            };
        };

An API endpoint can be tested using the api::invoke function, which takes either the path as a single string or the path along with a request body. It can also be tested via CURL or other means by directly using the endpoint along with the namespace and database in the headers.

api::invoke("/test");

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

{
	body: {
		request: NONE,
		response: 'The server works'
	},
	headers: {
		expires: '2026-10-18T04:51:11.128290Z',
		"last-modified": '2026-10-14T04:51:11.127573Z',
		"x-surreal-request-id": 'a93b6dcd-adc8-422a-8009-83edcedcbc39'
	},
	request_id: 'a93b6dcd-adc8-422a-8009-83edcedcbc39',
	status: 200
}

-------- Query --------

{
	body: {
		request: {
			give: 'me',
			hi: 'please',
			the: 'information'
		},
		response: 'The server works'
	},
	headers: {
		expires: '2026-10-18T04:51:11.129009Z',
		"last-modified": '2026-10-14T04:51:11.129008Z',
		"x-surreal-request-id": '2aae59a3-aa51-4aa3-bf50-a161332369bc'
	},
	request_id: '2aae59a3-aa51-4aa3-bf50-a161332369bc',
	status: 200
}

Each HTTP method may appear in at most one FOR clause on a DEFINE API statement. Overlapping methods - including duplicates inside a comma-separated list - are rejected at define time with a clear error instead of silently routing to only the first matching action.

DEFINE API "/foo"
  FOR get THEN $x
  FOR get THEN $y;
//- Error: duplicate `get` across FOR clauses

The path of a DEFINE API statement can be static, such as "/test", dynamic, or the remainder of a URL.

A dynamic path uses a : (colon) followed by a name, which will match on anything passed in at that section of a path.

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");

The first two api::invoke calls return the body with a status of 200. The third returns the response below with a status of 404, as :anything_goes only applies to a single path segment.

Sample output
{
	body: 'Not found',
	headers: {},
	request_id: '5dbef2f3-f643-4995-8545-fff364f78a96',
	status: 404
}

To match on the remainder of a URL, change the : (colon) to a * (star).

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 api::invoke calls now return the body with a status of 200, such as the following.

Sample output
{
	body: {
		some: 'data'
	},
	headers: {
		"x-surreal-request-id": 'ed05a73c-05e0-4b46-8ee9-19cab2098fa0'
	},
	request_id: 'ed05a73c-05e0-4b46-8ee9-19cab2098fa0',
	status: 200
}

A dynamic segment can name a type after its name, such as :id<number>. The segment then matches only a value that converts to the type, and $request.params.id holds the converted value: /items/789 matches "/items/:id<number>" with id set to 789, and /items/abc does not match it.

When more than one path matches a request, the most specific path runs. Each segment of a path adds to its score, and a fixed segment adds the most, then a typed dynamic segment, then an untyped dynamic segment, and a * remainder adds the least. The path with the highest score runs. With the following three definitions, /users/me runs the first, /users/42 the second, and /users/42/posts the third:

DEFINE API "/users/me" FOR get THEN { { body: 'me' } };
DEFINE API "/users/:id<number>" FOR get THEN { { body: $request.params.id } };
DEFINE API "/users/*rest" FOR get THEN { { body: 'rest' } };

The PERMISSIONS clause decides who may call an endpoint, and can differ for each method. When it is left out, it is PERMISSIONS FULL, so anyone who can reach the server can call the endpoint, including a client that has not signed in, whether or not guest access is allowed.

By default, the body runs without table and field permission checks, like the body of an event, so it can read and write records that the caller cannot reach directly. This is the SECURITY DEFINER mode, which can be written out or left out. Inside the body, $auth is still the caller: a record user's record ID, or NONE for a client that has not signed in. In the example below, a record user cannot select from secret_note, but can read it through the endpoint, while a client without credentials is refused:

DEFINE TABLE secret_note PERMISSIONS NONE;

DEFINE API "/notes"
	FOR get
		PERMISSIONS WHERE $auth != NONE
		THEN {
			{ status: 200, body: <string> (SELECT VALUE text FROM secret_note) }
		};
Warning

Because the body of a SECURITY DEFINER endpoint is not limited by table permissions, the endpoint returns whatever its body selects to everyone its PERMISSIONS clause lets in. Set PERMISSIONS on every endpoint that reads or writes data, and check $auth in the body where different callers should see different records.

A SECURITY DEFINER endpoint is the opposite of a defined function, whose body runs with the caller's permissions. A SECURITY INVOKER endpoint behaves like a function.

Available since: v4.0.0

With SECURITY INVOKER, the body runs with the permissions of the caller, so its queries return the same records and fields that the caller gets from a direct query. The clause applies to the whole endpoint: every method, its FOR any handler and its middleware. The body never has more access than the user who defined the endpoint.

The endpoint below is the same as the one above, with SECURITY INVOKER added. A record user who calls it gets an empty array, because secret_note has PERMISSIONS NONE, while the SECURITY DEFINER endpoint above returns every note to the same user.

DEFINE API "/my-notes"
	FOR get
		PERMISSIONS WHERE $auth != NONE
		THEN {
			{ status: 200, body: <string> (SELECT VALUE text FROM secret_note) }
		}
	SECURITY INVOKER;
Output for a record user
[]

INFO FOR DB and exports show SECURITY INVOKER after the methods of the definition, before any COMMENT, and leave the clause out for a SECURITY DEFINER endpoint. DEFINE API OVERWRITE without the clause returns the endpoint to SECURITY DEFINER. To change only the mode, use ALTER API, which keeps the current mode when the clause is left out:

ALTER API "/my-notes" SECURITY DEFINER;

Available since: v4.0.0

A handler that fails answers with an error response, usually a 500, and keeps none of what it wrote. Its records, its index entries and the writes of any events it set off are rolled back, while the rest of the transaction it ran in carries on. The same applies to each middleware call together with the part of the chain that it called.

DEFINE INDEX unique_v ON t FIELDS v UNIQUE;
CREATE t:0 SET v = 0;

DEFINE API "/two" FOR post THEN {
	INSERT INTO t [{ id: 1, v: 1 }, { id: 2, v: 0 }];
	{ status: 200 }
};

api::invoke("/two", { method: "post" }).status;
//- 500

SELECT id, v FROM t;
//- [{ id: t:0, v: 0 }]

The second record breaks the unique index, so the handler fails. Before 4.0, the response was the same but the first record stayed in the table, and so did the second one, which the index did not list.

Custom middleware can be used in addition to the functions listed above.

A custom middleware function is defined in the same way as any other user-defined function.

Each such function automatically receives two arguments:

  • An object that will contain the user request.

  • A function (a closure) that can be called to get the current state of the response to be returned.

The parameter names $req for the first and $next for the second are commonly used, though the actual parameter names are of no consequence.

The output of such a function must be an object. This is used to pass the response on to the next middleware function, or on to the actual response if there is no middleware left to call.

DEFINE FUNCTION fn::middleware_function($req: object,
  $next: function) -> object {};

These functions can take additional arguments on top of the required two.

DEFINE FUNCTION fn::middleware_function($req: object,
  $next: function,
  $some_string: string) -> object {};

Here is an example of an API endpoint without any middleware.

DEFINE API "/custom_response"
    FOR get
        THEN {
            {
                status: 200,
                body: {
                    num: 1
                }
            };
        };

Calling api::invoke("/custom_response") will return the following.

Sample output
{
	body: {
		num: 1
	},
	headers: {
		"x-surreal-request-id": '90ecf16c-d3d5-4a06-825f-c54c5af5afbf'
	},
	request_id: '90ecf16c-d3d5-4a06-825f-c54c5af5afbf',
	status: 200
}

The next example adds a middleware function that does the following:

  • Calls the $next closure on the $req object. This will return the value after THEN: the original return value.

  • Returns an object in which the num field inside body is increased by one.

DEFINE FUNCTION fn::increment_num($req: object,
  $next: function) -> object {
    LET $res = $next($req);
    $res + { body: { num: $res.body.num + 1 } }
};

DEFINE API "/custom_response"
    FOR get
        MIDDLEWARE
            fn::increment_num()
        THEN {
            {
                status: 200,
                body: {
                    num: 1
                }
            };
        };

Calling api::invoke("/custom_response") will now return a modified output in which num is equal to 2.

Sample output
{
	body: {
		num: 2
	},
	headers: {
		"x-surreal-request-id": 'a4fa9a57-f1ac-4eb1-8433-cce37a19a5e0'
	},
	request_id: 'a4fa9a57-f1ac-4eb1-8433-cce37a19a5e0',
	status: 200
}

Here is an example of the same endpoint with an extra custom middleware function which adds an x-called-at header to the response to show when the API endpoint was called.

DEFINE FUNCTION fn::start_timer($req: object,
  $next: function,
  $called_at: datetime) -> object {
    LET $res = $next($req);
    $res + { headers: { "x-called-at": <string>$called_at } }
};

DEFINE FUNCTION fn::increment_num($req: object,
  $next: function) -> object {
    LET $res = $next($req);
    $res + { body: { num: $res.body.num + 1 } }
};

DEFINE API "/custom_response"
    FOR get
        MIDDLEWARE
            fn::start_timer(time::now()),
            fn::increment_num()
        THEN {
            {
                status: 200,
                body: {
                    num: 1
                }
            };
        };

api::invoke("/custom_response");

The output will look something like this.

Sample output
{
	body: {
		num: 2
	},
	headers: {
		"x-called-at": '2026-10-14T04:51:23.959554Z',
		"x-surreal-request-id": 'ba447d57-24e6-4cf4-9e8b-68076b040980'
	},
	request_id: 'ba447d57-24e6-4cf4-9e8b-68076b040980',
	status: 200
}

Was this page helpful?