RESTful Endpoints aka APIs
Introduction
Speedy is a Rapid Application Development platform that offers quickest way to develop, and host RESTful APIs. It does this via RESTful
APIs itself, in other words, the entire development lifecycle of RESTful
API is done over /rest
endpoint of Speedy itself. All the defined API/endpoints become available instantaneously over /uri/rest/
. All of the Speedy’s RESTful APIs have database in common and provide CRUD abstraction over it. Custom business logic is supported with Java Script programming.
Supported Keywords
The following are the set of keywords to be used with definition creation.
uri - URI on which this def will be available
do - Do this when called for SQL/Func
method - HTTP Method type GET/PUT/POST/DELETE
version - Version of this definition
blang - Language used for functions js/tengo
before - Execute this func before the 'do' part
bver - Version of the before function
alang - Language used for functions js/tengo
after - Execute this function after the 'do' part
aver - Version of the after function
doc - Documentation associated with the definition
Speedy definitions
Let’s consider few examples to understand how to create a definition and make calls to it. We would like to draw your attention to one point before we proceed any further. Words like ‘definition’, ‘endpoint’ and ‘API’ all mean one and the same in the end, they represent the stages of metamorphosis which leads to a RESTful API creation. A “definition” is the code written to build an “endpoint”, and “API” is how an “endpoint” is invoked or executed.
Consider the following table, under the PostgreSql schema tiger
. You don’t need to create this, speedy
in evaluation mode creates this schema, table and sample rows automatically. This is given just for reference.
CREATE SCHEMA tiger;
CREATE TABLE IF NOT EXISTS tiger.employee(
-- Employee ID, automatically incremented
id SERIAL PRIMARY KEY,
-- Name of the employee
name VARCHAR(100) NOT NULL,
-- Date of join, time stamp
doj TIMESTAMP NOT NULL,
-- Employee is currently active or not
active BOOLEAN NOT NULL DEFAULT true,
-- Date of exit, if employee is not active
doe TIMESTAMP
);
CREATE INDEX idx_tiger_emp_status ON tiger.employee(active);
Let’s go ahead and create ‘definitions’ to perform CRUD operations on this table.
Creating a Speedy definition
First we shall build an API to create a new employee record. In SQL to do that we can use the following statement.
INSERT INTO tiger.employee(name, doj) VALUES ('Scott Mayer', NOW());
Now, let’s convert this SQL statement into a Speedy ‘definition’. If you are planning on using Postman put the following into body of the request; in general beware of double single quote escapes, '\''
kind.
!{ uri }!
sample/tiger/employee
!{ do }!
INSERT INTO tiger.employee(name, doj) VALUES ('?:name', NOW()) RETURNING id;
!{ version }!
1
!{ method }!
post
For curl
the following payload is just fine, just be mindful of this factor throughout the article.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee
!{ do }!
INSERT INTO tiger.employee(name, doj) VALUES ('\''?:name'\'', NOW()) RETURNING id;
!{ version }!
1
!{ method }!
post'
That’s it! Now let’s go over it in detail. curl --location --request POST 'localhost:3023/rest'
implicitly means we are “creating” a RESTful definition on /rest
endpoint, of HTTP method type POST. One could be running the Speedy server anywhere, in such case localhost
should be replaced with the right IP address or FQDN. The ‘definition’ details are enclosed within --data-raw
let’s understand it one by one.
All keywords within Speedy starts and ends with !{
and }!
. This has been introduced from v.6.8 onwards to help developers with better debug message while creating JavaScript/Tengo based functions. They not only offer better code readability but also offer easy development/maintainence cost. While crafting the actual payload the positional parameters aka custom keywords can also be enclosed within these, bare with it for a minute or two, it will be clear shortly.
"INSERT INTO tiger.employee(name, doj) VALUES ('?:name', NOW()) RETURNING id;
, this is our template SQL statement. ?:name
, tells Speedy that when actual payload is sent, look for a field by name “name”, and use its value inside the SQL statement. Observe that SQL statement has RETURNING id;
at the end. This is a very nice feature of PostgreSQL which returns the auto-incremented employee id value, upon INSERT
.
"sample/tiger/employee"
, with this we are informing Speedy that we would like to associate, the SQL statement given above on this path.
"version" -> "1"
, is self-explanatory; associates “1” as version number for this API definition. Speedy definition version is string type, so one can fancy any naming nomenclature, for example, “v1”, “v.1.0”, “v/1” are all valid version numbers. The same must be used while invoking the API too. We will see that in a while.
"method" -> "post"
, informs Speedy to make this definition available only with HTTP method POST. Now, let’s give the API a spin.
Invoking the API
curl -L -X POST 'localhost:3023/uri/rest/sample/tiger/employee' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ name }!
Ken Thompson
!{ version }!
1'
Above command in curl
invokes the API we just created. Notice the URI we are trying to reach, it is the same one we just defined as part of the definition. Definitions are created on /rest
and they are hosted automatically on /uri/rest
instantaneously.
Now for the second part. Look through the payload that is being sent to the API endpoint. The payload has name
and version
in it. This is precisely what we talked about custom keywords in the payload. Version has the same value we used during creation of ‘definition’ above. This helps Speedy to choose the right definition to be invoked for the same URI. “name”, is a part of Speedy’s keywords, if you notice. It comes from the definition itself, in the “do” section of definition it is mentioned, as part of template. That’s how dynamic values are bind into SQL statements. Executing the above curl
command gives the following output (something similar to it).
{"id":30}
This means that our API invocation was successful, and employee ID assigned to “Ken Thompson” is 30. Simple, right!
An API to read
So far we have covered the “Create” part, now let’s create few definitions to “Read” too.
List all active employees.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee
!{ do }!
SELECT * FROM tiger.employee;
!{ version }!
1
!{ method }!
get'
Invoke the API
curl -L -X GET 'localhost:3023/uri/rest/sample/tiger/employee?version=1'
Output
[
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:29.873735Z",
"id": 28,
"name": "Dunes High"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:41.948271Z",
"id": 29,
"name": "Mighty Joe"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:50.84025Z",
"id": 30,
"name": "Armstrong Joe"
}
]
Just like magic APIs are ready to use. Now is a good time to notice how we didn’t deploy any physical code, restart web-server, nor did any of the usual developer antics. That’s how simple Speedy is.
List employees with parameterised status.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee
!{ do }!
SELECT * FROM tiger.employee WHERE active = ?:status;
!{ version }!
1
!{ method }!
get'
And the request and output.
curl -X GET 'localhost:3023/uri/rest/sample/tiger/employee?version=1&status=false'
Notice how the “?:status”
in the SQL query was picked up from the URI query parameter! Speedy intelligently understands all these and works its magic.
[
{
"active": false,
"doe": "2022-10-29T16:39:38.413288Z",
"doj": "2021-01-19T09:39:38.113288Z",
"id": 4,
"name": "Jane Doe"
},
{
"active": false,
"doe": "2022-10-29T16:39:58.04551Z",
"doj": "2020-02-19T10:19:51.03551Z",
"id": 5,
"name": "Dow Jones"
}
]
API to Update/Modify
Now let’s cover the other part of CRUD, i.e. update.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee/lop
!{ do }!
UPDATE tiger.employee SET active = false, doe = NOW() WHERE id = ?:empid;
!{ version }!
1
!{ method }!
put'
That’s a simple ‘definition’ to mark an employee going on ‘lapse-of-pay’. Now let’s use it to put ‘Mike Elis’ on LOP.
curl -X PUT 'localhost:3023/uri/rest/sample/tiger/employee/lop' \
-H 'Content-Type: application/json' \
-d '{
!{ empid }!
2
!{ version }!
1'
Let’s check it!
curl -X GET 'localhost:3023/uri/rest/sample/tiger/employee?version=1&status=false'
We can see that ‘Mike’ is not longer among the ‘active’ employees list.
[
{
"active": false,
"doe": "2022-10-29T16:39:38.413288Z",
"doj": "2022-10-29T16:39:38.413288Z",
"id": 4,
"name": "Jane Doe"
},
{
"active": false,
"doe": "2022-10-29T16:39:58.04551Z",
"doj": "2022-10-29T16:39:58.04551Z",
"id": 5,
"name": "Dow Jones"
},
{
"active": false,
"doe": "2022-10-30T08:26:26.073998Z",
"doj": "2022-10-29T14:54:48.329645Z",
"id": 2,
"name": "Mike Elis"
}
]
API to Delete
Now let’s cover the other part of CRUD, i.e. delete.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee/exit
!{ do }!
DELETE FROM tiger.employee WHERE id = ?:empid;
!{ version }!
1
!{ method }!
delete'
That’s a simple ‘definition’ to mark an employee who is leaving the organisation; in reality such informations are preserved with status, this is just a tutorial for practise. Now let’s use it to send-off ‘Mike Elis’.
curl -X DELETE 'localhost:3023/uri/rest/sample/tiger/employee/exit' \
-H 'Content-Type: application/json' \
-d '
!{ empid }!
2
!{ version }!
1'
Let’s check it!
curl -X GET 'localhost:3023/uri/rest/sample/tiger/employee?version=1&status=false'
[
{
"active": false,
"doe": "2022-10-29T16:39:38.413288Z",
"doj": "2022-10-29T16:39:38.413288Z",
"id": 4,
"name": "Jane Doe"
},
{
"active": false,
"doe": "2022-10-29T16:39:58.04551Z",
"doj": "2022-10-29T16:39:58.04551Z",
"id": 5,
"name": "Dow Jones"
}
]
Advanced Topics
In this section let’s look at some of the advanced coding with Speedy. We assure you that it will be simple, as it is so far!
Custom Logic with Speedy
Let’s take a look at a well-designed RESTful API, the following figure depicts one such in logical blocks.
Within the ‘code’ of most of the APIs, there are three sections, first, pre-processing of given user data like validation, transformation, adding composite or derivative fields etc., second, persist the information with database or get results from it or both, third, apply some custom logic to the obtained data and return the resultant to User.
So far we have seen direct SQL statement bindings to the URI with Speedy. How do we build some custom logic around it? Speedy offers simple and elegant solution to that via JavaScript functions. Pre-processing business logic could be wired with an independent JavaScript function with the help of before
family of keywords, and Post-processing logic in the same way with after
family of keywords.
As a recap, before
family has blang
and bver
keywords, and after
family similarly has alang
and aver
. before
/ after
holds the name of the function, while blang
/ alang
holds the language in which the function is implemented, for now it is always js
. bver
/ aver
holds the version of the function respectively.
Engaging “before"
to action
Let’s define a simple name swapping function with the following curl
command.
curl -L -X POST 'localhost:3023/func' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/swapname
!{ name }!
swapname
!{ do }!
function swap(a) {
var c = a['\''name'\''].split('\'' '\'');
a['\''name'\''] = c[1] + '\'' '\'' + c[0];
}
swap(args);
!{ lang }!
js
!{ version }!
1
'
The entry point function within the JavaScript code should always take a parameter with the name args
only, as Speedy will pass the data over it. Now let’s execute the function to see whether it works!
curl -L -X POST 'localhost:3023/uri/func/sample/tiger/swapname' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ name }!
Wiss Jane
!{ lang }!
js
!{ version }!
1
'
And the result is,
{
"lang": "js",
"name": "Jane Wiss",
"version": "1"
}
Notice that it does what we have coded it for, that is, swap the first and second part of the name. Speedy always returns the entire body that is sent to it as part of the reply for Function endpoints. This is done in-order to make sure dynamic version on the server doesn’t affect the client. It’s a feature not a bug!
Now, let’s wire this function with our “Create” definition, defined earlier. Consider the following code for reference.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee/func/before
!{ do }!
INSERT INTO tiger.employee(name, doj) VALUES ('\''?:name'\'', NOW()) RETURNING id;
!{ version }!
1
!{ method }!
post
!{ before }!
sample/tiger/swapname
!{ blang }!
js
!{ bver }!
1'
Give it a go, with the following curl
command.
curl -L -X POST 'localhost:3023/uri/rest/sample/tiger/employee/func/before' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ name }!
Jackson Tike
!{ version }!
1'
By querying the result using the previous GET call we get the following. As we can see Speedy invoked the JavaScript function seamlessly and modified the supplied name from “Jackson Tike” to “Tike Jackson” before inserting it into the database table.
[
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:29.873735Z",
"id": 28,
"name": "Dunes High"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:41.948271Z",
"id": 29,
"name": "Mighty Joe"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:50.84025Z",
"id": 30,
"name": "Armstrong Joe"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T20:31:53.110039Z",
"id": 31,
"name": "Ken Thompson"
},
{
"active": true,
"doe": null,
"doj": "2023-01-08T22:28:03.726877Z",
"id": 36,
"name": "Tike Jackson"
}
]
Pro Tip: database functions and procedures can do a lot more than often one can imagine. If a certain thing must be one on a large set of data at the database level, consider using ‘Procedure’ or ‘Function’. Here are links to PostgreSql Procedure (https://bit.ly/3sHb1Km) and Function (https://bit.ly/3Wk3hvi). We have done some amazing timesaving and performant coding using them.
Engaging “after” to action
Let’s go ahead and write a similar functionality to swap
of name that we did earlier. But, this time let’s apply it against a result from a SQL query from database.
curl -L -X POST 'localhost:3023/func' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/reversename
!{ name }!
reversename
!{ do }!
function swap(a) {
var c = a['\''name'\''].split('\'' '\'');
a['\''name'\''] = c[1] + '\'' '\'' + c[0];
}
function rearrange(m) {
m.forEach(swap);
return m;
}
function extract(r) {
var m = JSON.parse(r['\''result'\'']);
r['\''result'\''] = JSON.stringify(rearrange(m));
}
extract(args)
!{ lang }!
js
!{ version }!
1'
Few important concepts to understand with this example.
- Speedy will pass the request (body sent for the API call) and result from database as
map
orObject
in JavaScript terms to the entry point function. - The database result is assigned as a value to key
result
on theargs
JSObject
. This is will in JSON format, but not inObject
format, it will be a plain string. - So, it becomes imperative to convert the JSON string to Object and back to string with
JSON.parse
andJSON.stringify
functions of JavaScript. - JavaScript function used for
after
keyword, must remember to set the value back, provided that is desired.
Now let’s create a ‘definition’ which will use this ‘function’ in runtime.
curl -L -X POST 'localhost:3023/rest' \
-H 'Content-Type: text/plain' \
--data-raw '
!{ uri }!
sample/tiger/employee/func
!{ do }!
SELECT * FROM tiger.employee WHERE active = true;
!{ version }!
1
!{ method }!
get
!{ after }!
sample/tiger/reversename
!{ alang }!
js
!{ aver }!
1'
Time to call the API endpoint to put the whole thing into action.
curl -L -X GET 'localhost:3023/uri/rest/sample/tiger/employee/func?version=1'
[
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:29.873735Z",
"id": 28,
"name": "High Dunes"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:41.948271Z",
"id": 29,
"name": "Joe Mighty"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:50.84025Z",
"id": 30,
"name": "Joe Armstrong"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T20:31:53.110039Z",
"id": 31,
"name": "Thompson Ken"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T20:32:19.5654Z",
"id": 32,
"name": "Thompson Ken"
}
]
Notice how the JavaScript function changed the First and Last name of every record before sending it back. Here is the original output without the function.
curl -L -X GET 'localhost:3023/uri/rest/sample/tiger/employee/func?version=1'
[
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:29.873735Z",
"id": 28,
"name": "High Dunes"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:41.948271Z",
"id": 29,
"name": "Joe Mighty"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T13:36:50.84025Z",
"id": 30,
"name": "Joe Armstrong"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T20:31:53.110039Z",
"id": 31,
"name": "Thompson Ken"
},
{
"active": true,
"doe": null,
"doj": "2023-01-07T20:32:19.5654Z",
"id": 32,
"name": "Thompson Ken"
}
]
It is also possible to use both before
and after
in a single “definition”, depending on the usecase.