README
¶
EasyREST
EasyREST is a lightweight, extensible REST service that provides a unified API for performing CRUD and aggregating queries on relational databases. Its key innovation is the use of a plugin system that allows you to connect to virtually any database (or data source) simply by writing a plugin that conforms to the EasyREST interface. Out of the box, an SQLite plugin is provided, but you can easily add plugins for MySQL, PostgreSQL, Oracle, or any other database.
EasyREST handles authentication via JWT, validates and sanitizes request parameters at the controller level, and then passes 100% safe SQL expressions to the plugins. The service supports complex queries including aggregations, aliasing, advanced WHERE conditions, and even the use of context variables (passed via a Common Table Expression or CTE) that allow you to influence query behavior dynamically.
Table of Contents
Architecture
EasyREST’s design emphasizes simplicity, extensibility, and security. It follows a modular architecture where the core server is responsible for:
- Authentication & Authorization: Validates JWT tokens and verifies scopes.
- Parameter Validation: Ensures that all parameters (select, where, ordering, etc.) are safe before forwarding to the plugins.
- Plugin Orchestration: Loads and manages DB plugins based on environment configuration.
- Context Propagation: Extracts contextual information (e.g., timezone, HTTP headers, JWT claims) from each request and passes them to plugins via a CTE (Common Table Expression) named
erctx.
Overall Architecture Diagram
flowchart TD
A[HTTP Request] -->|REST API| B[EasyREST Server]
B --> C{Authentication & Validation}
C -- Valid --> D[Process Request]
C -- Invalid --> E[401/400 Response]
E --> N[HTTP Response]
D --> J[Plugin Client RPC Call]
J --> K["DB Plugin (e.g., SQLite)"]
K --> F[Build SQL Query]
F --> G{Apply Context?}
G -- Yes --> H["Wrap Query with CTE (erctx)"]
G -- No --> I[Unmodified Query]
H --> L[Database]
I --> L[Database]
L --> M[Return Results to DB Plugin]
M --> J2[Return Results to Plugin Client RPC Call]
J2 --> B2[Return to EasyREST Server]
B2 --> N[HTTP Response]
Plugin Communication
- RPC Communication: EasyREST uses Hashicorp's go-plugin system to load and communicate with plugins. The server launches plugin processes based on environment variables (e.g.
ER_DB_TEST) and establishes an RPC connection. - Data Exchange: The server sends validated and sanitized SQL expressions and additional context data to the plugin. This context is injected into the query using a CTE so that plugins can optionally use the provided parameters.
- Type Registration: For secure and reliable encoding/decoding over RPC, custom types (e.g.,
time.Timeand flattened JWT claims) are registered with the gob package.
Authentication and Authorization
- JWT Authentication: Requests must include a Bearer token. The server verifies the token signature (using a secret from configuration) and extracts claims.
- Scope-based Authorization: The service verifies that the token’s claims include the required scope (e.g.,
users-readfor GET operations,users-writefor modifications or widereadandwritefor all table names). - Token Claims: The claims are flattened and passed to plugins (under the key
claimsin the context map) so that plugins can leverage user-specific information if needed.
Query Capabilities
EasyREST supports a wide range of SQL query features by converting URL query parameters into SQL expressions after performing strict validation.
Selecting Fields and Aliasing
- Basic Selection:
Use theselectparameter to specify one or more columns. For example:/api/test/users/?select=id,name - Field Aliasing:
To alias a column, use the syntaxalias:field.
For example,
becomes/api/test/users/?select=username:nameSELECT name AS username FROM users - SQL Functions:
Aggregation functions are supported. For instance,
results in:/api/test/orders/?select=count()
You can also use functions with aliasing:SELECT COUNT(*) AS count FROM orders
which translates to:/api/test/orders/?select=total:amount.sum(),order_dateSELECT SUM(amount) AS total, order_date FROM orders GROUP BY order_date
Aggregating Queries
- EasyREST supports common SQL aggregate functions such as
COUNT(),SUM(),AVG(),MIN(), andMAX(). - Aggregated fields are automatically excluded from the GROUP BY clause.
- Non-aggregated fields that appear in the
selectparameter are added to a GROUP BY clause automatically.
WHERE Conditions
- Syntax:
Conditions are specified using parameters prefixed withwhere.. For example:
This generates:/api/test/users/?where.eq.name=AliceWHERE name = 'Alice' - Operators:
Supported operators include:eq(equals)neq(not equals)lt(less than)lte(less than or equals)gt(greater than)gte(greater than or equals)like(LIKE)ilike(ILIKE)is(IS)in(IN)
- erctx Fields:
You can compare a column with a context variable. For example,
Here, the string/api/test/users/?where.eq.name=erctx.claims_suberctx.claims_subis not treated as a literal but is injected into the SQL query as a column reference from the CTE. - Validation:
The server validates that only allowed operators are used and that field names (including those starting witherctx.) are valid.
Context Variables via erctx
-
Context Extraction:
The server extracts contextual information from the HTTP request. Context keys include:- timezone: Extracted from the
Preferheader (e.g.,Prefer: timezone=America/Los_Angeles). If not provided, a default value (e.g., "GMT") from the server configuration is used. - headers: A map of all HTTP headers, with keys converted to lowercase and values concatenated if repeated.
- claims: The JWT token claims, flattened to a plain map with lowercase keys.
- timezone: Extracted from the
-
CTE Injection:
If any select field or where condition references a string beginning witherctx., the server wraps the SQL query with a CTE namederctxthat provides columns for each flattened context variable.For example, given a context:
{ "timezone": "America/Los_Angeles", "claims": { "sub": "Alice" } }The server may wrap a query like:
SELECT id, name FROM users WHERE name = erctx.claims_subas:
WITH erctx AS ( SELECT ? AS timezone, ? AS claims_sub ) SELECT id, name FROM users, erctx WHERE name = erctx.claims_subThe context values are passed as parameters.
Data Insertion, Updates, and Deletion
- Insertion (POST):
Data is passed as JSON in the request body. The server builds anINSERT INTOstatement after validating the column names. If any value in the data references a context variable (contains"erctx."), the INSERT query is wrapped with a CTE. - Update (PATCH):
The update request requires a JSON object in the request body (with columns to update) and optional where conditions in the query string. Similar to insertion, if the data or where conditions reference a context variable, the query is wrapped with a CTE. - Deletion (DELETE):
Deletion is based solely on the where parameters. If any where value contains"erctx.", the query is wrapped with a CTE.
Configuration Parameters
EasyREST is configured via environment variables. The primary configuration parameters include:
-
ER_PORT:
The port on which the server listens (default:8080). -
ER_CHECK_SCOPE:
Enables or disables scope checking. When set to"1", the server verifies that the JWT token has the proper scopes (e.g.,users-readfor GET andusers-writefor modifications). If unset or any other value, scope checking is disabled (default: enabled). -
ER_TOKEN_SECRET:
The secret used to sign/verify JWT tokens. -
ER_TOKEN_USER_SEARCH:
The claim key used to extract the user ID from the JWT token (default:"sub"). -
ER_NO_PLUGIN_LOG:
Controls plugin logging. When set to"1", plugin logging is enabled; otherwise, it is disabled. -
ER_DEFAULT_TIMEZONE:
The default timezone to use if none is provided via thePreferheader (e.g.,"GMT"or"America/New_York"). -
ER_DB_:
For each database connection, you set an environment variable in the formER_DB_TEST,ER_DB_PROD, etc. The value should be in the format:sqlite://path/to/database.dbor for other plugins, a URI scheme that the plugin understands.
Examples
Multiple SQLite Databases with Different Structures
Imagine you have two SQLite databases:
-
Test Database (
ER_DB_TEST):- Path:
./test.db - Table:
users
Structure:id(auto-increment),name(text),update_field(text)
- Path:
-
Orders Database (
ER_DB_ORDERS):- Path:
./orders.db - Table:
orders
Structure:id(auto-increment),items(integer),order_date(date),amount(real)
- Path:
Set environment variables:
export ER_DB_TEST="sqlite://./test.db"
export ER_DB_ORDERS="sqlite://./orders.db"
export ER_TOKEN_SECRET="your-secret"
export ER_TOKEN_USER_SEARCH="sub"
export ER_DEFAULT_TIMEZONE="GMT"
Query Examples:
-
Selecting Fields with Aliases:
GET /api/test/users/?select=username:name,update_field→
SELECT name AS username, update_field FROM users; -
Aggregating Queries:
GET /api/orders/?select=total:amount.sum(),order_date→
SELECT SUM(amount) AS total, order_date FROM orders GROUP BY order_date; -
Where Conditions with Context Variables: Suppose the JWT token has a claim
"sub": "Alice". The following request:GET /api/test/users/?select=id,name&where.eq.name=erctx.claims_subWith a context built from the token claims, the query is wrapped with a CTE so that
erctx.claims_subreferences the value"Alice". -
Data Insertion:
POST /api/test/users/ Body: [{"name": "Bob", "update_field": "new"}]If one of the field values references
erctx.(for example, if you want to set a column based on a context variable), the query is wrapped accordingly.
Building and Running
Building the Server
Ensure you have Go installed (version 1.23 or later).
To build the server, run:
make server
This will produce an executable named easyrest-server in bin folder.
Building a Plugin (SQLite)
To build the SQLite plugin, navigate to the plugin folder and run:
make plugin-sqlite
This will produce an executable named easyrest-plugin-sqlite that the server will look for (via exec.LookPath) when an environment variable like ER_DB_TEST is set to a URI starting with sqlite://.
Running Tests and Benchmarks
EasyREST includes an extensive test suite. To run all tests, use:
make test
To run benchmarks:
make bench
The tests include unit tests for CRUD operations, query aggregation, and context propagation. Make sure your environment variables are set appropriately for testing.
Developing External Plugins
To create an external plugin (e.g., a MySQL plugin):
-
Create a New Repository:
For instance, create a repository namedeasyrest-plugin-mysql. -
Import the Common Module:
In your plugin’sgo.mod, add:require github.com/onegreyonewhite/easyrest v0.1.0Then, import the plugin package:
import easyrest "github.com/onegreyonewhite/easyrest/plugin" -
Implement the Interface:
Implement theDBPlugininterface defined ineasyrest/plugin. Use the same handshake configuration (accessible aseasyrest.Handshake). -
Structure Your Repository:
- Have a main package (e.g.,
cmd/plugin-mysql/main.go) that registers your plugin using:func main() { impl := &mysqlPlugin{} plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: easyrest.Handshake, Plugins: map[string]plugin.Plugin{ "db": &easyrest.DBPluginPlugin{Impl: impl}, }, }) } - Organize your code so that the core logic is in packages that can be independently tested.
- Have a main package (e.g.,
-
Build and Deploy:
Build the plugin binary withgo buildand ensure it is discoverable by EasyREST (via naming conventions and PATH configuration).
Changes in EasyREST Repository
In the EasyREST core, nothing changes except that you now reference plugins by their binary names (e.g., easyrest-plugin-mysql) using exec.LookPath. Document the expected naming convention so that external plugins can be built and integrated independently.
Summary
EasyREST is a simple yet powerful service for exposing database functionality via a REST API. It is built to be secure by validating and sanitizing all inputs on the server side, while delegating the actual query execution to plugins. The plugin architecture makes it easy to support multiple databases, and the context feature (via CTEs) allows you to pass dynamic parameters (like timezone, headers, and token claims) to your queries. With flexible query capabilities—including field aliasing, aggregate functions, and complex WHERE conditions—EasyREST provides a robust foundation for building data-driven applications.
Feel free to contribute or extend the functionality by writing new plugins for different databases or enhancing the query capabilities. Enjoy using EasyREST!
License
EasyREST is licensed under the terms of the Apache License 2.0. See the file "LICENSE" for more information.
Copyright 2025 Sergei Kliuikov