GRIT is a minimalist microservice framework built in pure Go. Designed to simplify RESTful API development without sacrificing performance, it draws inspiration from ALA while embracing Go's philosophy of simplicity, concurrency, and clarity.
git clone https://github.com/not-empty/grit-microframework-go.git
Bring up the service and its MySQL dependency:
docker-compose up -d
Enter the running container:
docker exec -it grit sh
Copy .env.example
to .env
and adjust:
APP_ENV=local # expose errors in response when "local"
APP_LOG=true # enable HTTP access logs
APP_NO_AUTH=true # disable auth (not for production)
APP_PORT=8001 # HTTP port
DB_DRIVER=mysql # only MySQL supported
DB_HOST=grit-mysql
DB_NAME=grit
DB_USER=user
DB_PASS=password
DB_PORT=3306
DB_MAX_CONN=100
DB_MAX_IDLE=10
DB_HOST_TEST=grit-mysql
DB_NAME_TEST=grit
DB_PASS_TEST=password
DB_PORT_TEST=3306
DB_USER_TEST=user
JWT_APP_SECRET=secret # JWT signing secret
JWT_EXPIRE=900 # expiration seconds
JWT_RENEW=600 # auto-renew threshold seconds
Also copy ./config/tokens.json.example
→ ./config/tokens.json
to configure valid tokens and contexts.
Run GRIT with:
run go main.go
Method | Path | Description |
---|---|---|
POST | /example/add |
Create a new record |
POST | /example/bulk |
Fetch specific records by IDs |
POST | /example/bulk_add |
Create up to 25 records in the same request |
GET | /example/dead_detail/{id} |
Get a deleted record by ID |
GET | /example/dead_list |
List deleted records (paginated) |
DELETE | /example/delete/{id} |
Soft-delete a record by ID |
GET | /example/detail/{id} |
Get an active record by ID |
PATCH | /example/edit/{id} |
Update specific fields |
GET | /example/list |
List active records (paginated) |
GET | /example/list_one |
List one record based on params |
POST | /example/select_raw |
Execute a predefined raw SQL query safely |
-
Generate a JWT
curl -i -X POST http://localhost:$APP_PORT/auth/generate \ -H "Content-Type: application/json" \ -H "Context: <your-context>" \ -d '{"token":"<token>","secret":"<secret>"}'
On success you get HTTP 204 with headers:
X-Token
: JWTX-Expires
: expiration timestamp
-
Make API calls
curl -i GET http://localhost:$APP_PORT/example/list \ -H "Authorization: Bearer <JWT>" \ -H "Context: <your-context>" \ -H "Accept: application/json"
Every valid response return your valid token or renews it if is needed, always in the headers:
X-Token
: new JWTX-Expires
: new expiration
Header | Description |
---|---|
X-Request-ID |
Unique ULID for the request |
X-Profile |
Profiling timer (seconds) |
X-Token |
JWT token (on auth or renew) |
X-Expires |
JWT expiration timestamp |
X-Page-Cursor |
Cursor for next page (string) |
By default endpoints return up to 25 items and include an X-Page-Cursor
header when more pages exist.
-
First page (no cursor):
curl -i GET "http://localhost:$APP_PORT/example/list" \ -H "Authorization: Bearer <JWT>"
HTTP/1.1 200 OK X-Page-Cursor: eyJsYXN0X2lkIjo... # opaque cursor Content-Type: application/json [ {"id":"1","name":"Alice"}, ... ]
-
Next page:
curl -i GET "http://localhost:$APP_PORT/example/list?page_cursor=<cursor>" \ -H "Authorization: Bearer <JWT>"
Once fewer than 25 records return, no X-Page-Cursor
is emitted (end of list).
Works on list and list_one endpoints
-
Order by any column:
?order_by=name&order=desc
-
Select fields: the default is all the fields
?fields=id,name,created_at
Example:
curl -i GET "http://localhost:$APP_PORT/example/list?order_by=age&order=asc&fields=id,name" \
-H "Authorization: Bearer <JWT>"
Works on list and list_one endpoints
Use filter
params:
?filter=age:eql:30&filter=name:lik:John
Supported operators:
eql
→=
neq
→!=
lik
→LIKE
(contains)gt
→>
lt
→<
gte
→>=
lte
→<=
btw
→BETWEEN
(value1,value2)nul
→IS NULL
nnu
→IS NOT NULL
in
→IN
(comma list)
Allows execution of pre-registered raw SQL queries with named parameters. Queries must be registered in your model.
1 - Register queries in app/repository/models/_raw.go:
package models
import "github.com/not-empty/grit-microframework-go/app/helper"
func init() {
helper.RegisterRawQueries("example", map[string]string{
// key is query name, value is SQL template
"count_active": `
SELECT COUNT(1) AS total
FROM example
WHERE age = :age
`,
})
}
2 - Request format:
POST /example/select_raw
Content-Type: application/json
{
"query": "count_active",
"params": {
"age" : 22
}
}
3 - Response:
[200 OK] with JSON array of rows (each row is an object)
[400 Bad Request] if query is unknown, parameters mismatch, or contains forbidden terms
[500 Internal Server Error] on execution errors
4 - Limitations & rules:
Allowed: only SELECT or WITH statements
Denied substrings: ;, --, /*, */
Denied keywords: drop, alter, truncate, delete, update, insert, create, merge, replace, grant, revoke, commit, rollback, savepoint, lock, unlock, exec, call, use, set, limit, offset, join
All named parameters (e.g. :id) in the query must be provided in the params object, and no extra parameters are allowed.
Maximum rows returned is 25 (hard-coded).
- New Domain (with DDL in
./cmd/sql/{name}.sql
):cd cmd/domain go run main.go -domain=name
Generated files:
app/repository/models/{name}_model.go
app/repository/models/{name}_raw.go
app/router/domains/{name}_domain.go
Generated code for new domains are test free since they are abstract of the basic implementations.
- Generate Route:
cd cmd/route go run main.go -route=name
Generated files:
app/controller/{name}_controller.go
app/router/routes/{name}_router.go
Generated code for new routes will counts toward coverage—tests since they are new logic.
You can add a new record by sending a POST
request to /example/add
, or use the /example/bulk_add
endpoint to add multiple records at once. By default, the API automatically generates a unique ID for each record.
However, if you prefer to use a custom ID, you can include the id
field in the request body. In that case, the API will use the provided ID and skip the automatic ID generation.
You can add validation in fields including the validation statement in models or in the fields comments in the DDL file before generating the domain:
Either way, you need to use the validate statement from https://github.com/go-playground/validator and its options.
You can add or change in the model just including or editing the validate statement and the choosed options on the selected fields:
type Example struct {
ID string `json:"id"`
Name string `json:"name" validate:"required,min=5"`
Age int `json:"age" validate:"required,number,gt=0,lt=100"`
LastLogin *helper.JSONTime `json:"last_login"`
CreatedAt *time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
}
Or you can add a comment -- validate in the sql DDL inside the cmd/sql folder and regerate the model (recommended):
CREATE TABLE example (
`id` CHAR(26) NOT NULL,
`name` TEXT NOT NULL, -- validate: "min=5" -- sanitize-html
`age` INT DEFAULT 0, -- validate: "required,number,gt=0,lt=100"
`last_login` DATETIME DEFAULT NULL,
`created_at` DATETIME DEFAULT NULL,
`updated_at` DATETIME DEFAULT NULL,
`deleted_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_example_deleted_at` (`deleted_at`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
GRIT automatically detects and omits any model fields that have a database‐side DEFAULT clause when inserting new records and these fields are not present i the request. In practice, this means:
If in your SQL you have a field with default value like:
...
status_name VARCHAR(20) NOT NULL DEFAULT 'active',
notes TEXT DEFAULT NULL,
...
And if you make an "/add" request without informing these fields, they will be omitted in the insert query, letting the database use his DEFAULT VALUE.
Run all tests with coverage:
./audit.sh
See ./tests/coverage/coverage-unit.html
for details.
For more request examples, see ./ops/curl.sh
. Suggestions and contributions welcome!
If you are using a REST API software like Insomnia or Postman you can import the ./ops/rest_environment.json
and ./ops/rest_collection.json
files.