Build REST and GraphQL APIs
Run an API as one function that reads SQL Database, writes through the Azion API, caches list responses, and reports latency and errors.
A development team builds the REST or GraphQL backend that its web and mobile clients call, with users in several regions and no server it wants to operate. The API keeps its records in a relational database, most calls read, and some calls write. This page deploys one function that implements every endpoint of a REST API, reads its records from SQL Database, writes them through the Azion API, and keeps the list response in cache for a short time. The result is measured by API response time per region, error rate under load, and the time from a code change to production.
This use case does not cover APIs that react to events instead of callers, which Build event-driven APIs covers, or security controls in front of an API that runs elsewhere, which Protect public APIs from abuse covers.
Prerequisites
- Azion CLI installed, with your personal token saved. To set it up, refer to Azion CLI quickstart.
- SQL Database enabled on your account, and the Edit SQL Database permission. The product is in Preview, so request access through Technical Support.
- A personal token for the database calls, separate from the one the CLI uses, because the function stores it. To create one, refer to Personal tokens.
- Node.js and a package manager, which the CLI uses to build the project.
- The names this page uses:
tasks-apifor the database and the response cache,/api/tasksfor the API path,TASKS_DB_IDandTASKS_SQL_TOKENfor the function’s environment variables, andapi.example.comfor the domain. The deploy prints axxxxxxxxxx.map.azionedge.netdomain; to serve the API on your own domain, refer to Add a custom domain to a workload. Replace each value with yours in every step.
Required products
| The API needs | Which means | Product | Documented in |
|---|---|---|---|
| Endpoints that run with no server to operate | One function, deployed with the Azion CLI, that the application runs on every request | Functions | Deploy a function with Azion CLI |
| Records that every endpoint reads and writes | Reads from a read replica of the database inside the function, and writes through the Azion API | SQL Database | Write rows to SQL Database from a function |
| A list response that is not rebuilt on every call | A response the function stores with the runtime Cache API, under a max-age | Cache | Cache a function’s response with the Cache API |
| Latency and errors on the API’s domain | The Requests and Status Codes dashboards filtered to the domain, and the Functions invocations | Real-Time Metrics | Build dashboards |
Reference architecture
This page builds the Single-service API on SQL Database: one function implements every endpoint and reads and writes one database in SQL Database.
Read the diagram from the function outward. Every request the application receives runs the same function, so the function is the one place that knows every endpoint. From there, the paths split by what the endpoint does: a response that every client may share goes to Cache, a read goes to a replica of the database, and a write goes to the main instance. KV Store sits beside the database for values read by key, such as a session, and the design works without it. Real-Time Metrics reads what the application records and changes nothing on the request path.
Dataflow
- A client’s request reaches the workload on the API’s domain, which hands it to the application, and a rule of the application runs the API function.
- The function matches the method and the path. A path outside
/api/tasksanswers404. - A
GETfor the task list is answered from the copy the function stored in Cache. When no copy exists, the function reads the rows and stores the new response for 60 seconds. - A
GETfor one task opens a connection to a read replica of the database withDatabase.open, which takes the database name and no token, so the data a read needs stays inside Azion. - A
POSTor aDELETEsends its statement to the database’s query endpoint of the Azion API, with the personal token the function reads from an environment variable, because the main instance is the only one that applies writes. The replicas then hold the change. - After a write, the function deletes the stored list response, so the next list request reads the rows again.
Components
- application: the Platform Resource that receives the API’s requests on its domain and routes them to the function with a Rules Engine rule.
- Functions: one function implements every endpoint. A deploy replaces the code of all the endpoints together, which is what makes the API one unit, so a deploy, a rollback, or a schema change reaches every endpoint at the same moment.
- SQL Database: holds the API’s relational data. A function reads through a read replica with no token, and writes go to the main instance through the Azion API with a personal token.
- KV Store: holds sessions and configuration read by key, a design option. It is eventually consistent and has no compare-and-set, so it carries values that tolerate a short delay, not the API’s records.
- Cache: stores the responses that every client may share, such as a list or a lookup, so a repeated call skips the database read.
- Real-Time Metrics: reports the API’s request time and status codes by domain, and the number of times the function ran.
Other designs for this use case
- Single-service API over an external database: for teams whose data already lives in a managed database such as Neon, MongoDB Atlas, TiDB, or Turso, which one function queries through its serverless driver or HTTP API. Every uncached call crosses to the external database, so its latency and availability enter the request and failure flows.
- Microservices API on Functions: for teams that split an API into services owned by different teams, each one a function with its own store, deployed and versioned on its own. A Rules Engine rule routes each path prefix to its service and services call each other over HTTP through the same rules, so a deploy changes one service and leaves the others on the version they had.
Configure the tasks database
The tasks API keeps its records in one table of a database named tasks-api. The function reads the database by its name and writes to it by its identifier, so this section creates the database through the API, whose create response returns that identifier. Azion Console can also create the database and run the statement in its Editor tab, as Create and manage databases shows.
To create the database:
The API answers 202. Keep data.id: it is the database identifier the function writes to.
Provisioning takes roughly 15 seconds. Send GET /v4/workspace/sql/databases/<database-id> until status reads created, then create the table and two rows to read back. completed is an integer column, 0 or 1, because the function reads it as a number:
The API answers 200 with "state": "executed" and one entry per statement in data. A statement that fails also answers 200, with error in place of results in its entry, so check each entry:
The tasks-api database holds the tasks table and two rows.
Configure the function’s write credentials
A function reads a database through a read-only replica, and a statement that writes fails there with attempt to write a readonly database. The API function therefore sends its writes to the Azion API, and it needs two values to do so: the database identifier and a personal token. Store both as Write rows to SQL Database from a function describes, with the API’s key names:
A variable reaches the function only after a deploy, so create both before the deploy in the next section.
Configure the API function
The API function is one ES Modules handler that implements every endpoint under /api/tasks. It reads with the Database class of the Azion.Sql global. Its writes follow Write rows to SQL Database from a function, and its list follows Cache a function’s response with the Cache API, with these values:
- Writes:
writeTaskssends one statement per call, withTASKS_DB_IDandTASKS_SQL_TOKEN, and throws on a failed request or a failed statement. - List cache: the cache
tasks-api, the key<origin>/api/tasks, andmax-age=60. EveryPOSTandDELETEdeletes that key after its write, so 60 seconds only bounds how long a list stays stale when that delete does not run.
Run azion init, enter tasks-api as the name, and select the Javascript preset and the Hello World template, as Deploy a function with Azion CLI shows. Then go to the project directory.
Replace the contents of index.js, the entrypoint the build reads, with the code below.
Run azion deploy from the project directory. The command builds the project, creates the application and the function, instantiates the function, and prints the domain that serves it.
The code makes three more decisions:
- Reads stay inside Azion.
Database.openneeds no token, and the?parameter carries the task ID as an integer, which the replica binds. Passing a JavaScript string as a parameter fails withunknown variant `String`. - The delete reports a missing task.
rows_writtencounts the rows the statement wrote, so0means no task carried that ID. - The list copy carries its own timestamp.
x-tasks-cached-atis set once, when the function stores the response, so two responses with the same value came from the same stored copy.
The API answers on the domain the deploy printed, every endpoint runs in the function, and the list response is cached for 60 seconds. The first deploy can take several minutes to answer from every location.
Verify the setup
Each check calls the API on its domain. A first deploy that does not answer yet is still propagating; wait a few minutes and retry.
-
The list endpoint reads the database. Request the list:
The response carries
200and the two rows the table holds: -
The list comes from the cached copy. Repeat the request within 60 seconds. The
x-tasks-cached-atheader carries the same value as in the first response. -
A write reaches the database. Create a task:
ShellThe response carries
201and the ID the database assigned:A
500here means the write failed. Read the message the function logged, as Query function console logs shows:Azion API answered 401points at theTASKS_SQL_TOKENvalue, and a database error string points at the statement. -
A write refreshes the list. Request the list again. The response carries a new
x-tasks-cached-atvalue and three tasks. -
A missing task answers 404. Delete the same task twice:
The first call answers
{"message":"Task deleted"}, and the second answers404with{"error":"Task not found"}.
Measuring results
| Metric | Where to read it | What working looks like |
|---|---|---|
| API response time per region | Average Request Time on the Requests dashboard of Real-Time Metrics, filtered by Host to the API’s domain and by Country. Refer to Filter a Real-Time Metrics dashboard | Comparable across the countries your clients call from, and stable as traffic grows |
| Error rate under load | HTTP Status Codes 5XX on the Status Codes dashboard, filtered to the API’s domain. Refer to Build dashboards | No 500 series rising with request volume; a rise points at failed writes, which the function logs |
| How often the API code runs | Total Invocations on the Functions tab, Edge Application Invocations series. Refer to Build dashboards | Tracks the API’s request count, since every request runs the function |
| Time from a code change to production | The deploy log that azion deploy links in Azion Console. Refer to How Azion CLI works | Each deploy completes, and the new code answers once it propagates, in about two minutes for a deploy after the first |
Best practices
-
Read through the replica and write through the API.
Database.openreaches a read replica with no token, and it refuses writes. Sending reads to the API instead spends the personal token on every call and adds a request the replica answers directly. For how the main instance and the replicas split the work, refer to How SQL Database works. -
Check every statement, not the HTTP status. A failed statement returns
200witherrorin its entry. A client that reads only the status reports a failed insert as a success, and the record is never written:JavaScript -
Give the function a token of its own, with a planned expiration. A personal token expires at the date chosen when it is created, and every write fails from that moment. A token used only by the function can be replaced and redeployed without touching the CLI’s token. For the expiration options, refer to Personal tokens.
-
Never build a statement from raw request text. The query endpoint takes SQL strings, so a title that carries a quote changes the statement the database runs. Quote text values and double their quotes, as
sqlTextdoes, and validate every field before it reaches a statement. -
Cache only what every client may read. A response stored with the Cache API is returned to any later request for the same key. Cache list and lookup responses that are the same for every client, and never a response built from a client’s credentials.