MicroQL is a declarative JSON-based query language for composing and orchestrating microservices. It automatically handles dependency resolution, parallel execution, error handling, and validation while providing a clean, expressive syntax for complex service interactions.
If you're a programmer and you want to achieve the low risk/high productivity of something like Zapier or n8n, but without all the baggage that those services come with, use MicroQL. You may 1) make yourself more productive, 2) enable a non-coder on the team to be able to compose queries, enabling a division of labor where you and other coders can focus on building out new services, or working on something else entirely.
import query from 'microql'
import users from './users.js'
import audit from './audit.js'
const result = await query({
given: {userId: 'user123'},
services: {users, audit},
queries: {
// depends on $.given, which is provided by the query definition
// so this will run first
profile: ['users', 'getProfile', {id: '$.given.userId'}],
// depends on $.profile, so MicroQL will run this after the profile query is complete
auditLog: ['audit', 'log', {action: 'profile_access', user: '$.profile'}]
}
})
console.log(result.profile) // {id: 'user123', name: 'John Doe', email: 'john@example.com'}
console.log(result.auditLog) // {timestamp: 1642533600000, action: 'profile_access', userId: 'user123'}
Data references imply dependency, and MicroQL will automatically execute queries as their dependencies become available. Circular references (unprocessable queries) are detected at query parse time and aborted.
$
- All query results in their present state - does not depend or wait on any queries$.given.field
- Access initial input data$.queryName
- Reference results from other queries, implies dependency on that query$.queryName.foo[0].bar
- Arbitrary (and safe) nested data access - returns undefined or value@
- Current context: May be a chain (result of previous step), an iterator value, or the error for anonError
handler@@
- Parent context,@@@
- grandparent context@.foo[0].bar
- nested paths work on context
Services are just objects containing asynchronous methods. They take an object containing named args and return a promise, with data up to the service's discretion. This allows you to easily wrap many kinds of API calls, databases, etc. and make them available to be orchestrated within a MicroQL query. If your target is well formed e.g. REST, perhaps you can programmatically create the service wrapper.
services: {
users: {
async getProfile({id}) {
return {id, name: 'John Doe', email: '[email protected]'}
}
},
audit: {
async log({action, user}) {
return {timestamp: Date.now(), action, userId: user.id}
}
}
},
You can 'pipe' values through chains, using '@' context, which in this case will refer to the previous value in the chain. Steps in a chain will run sequentially, but any paths referenced by the entire query tree (including the chain) will be considered dependencies of that query, and will determine the execution sequence of queries.
const result = await query({
given: {data: [1, 2, 3, 4, 5]},
// these are typically in other files, not inline, but presented here for a complete example
services: {
math: {
async double({values}) {
return values.map(x => x * 2)
},
async sum({values}) {
return values.reduce((a, b) => a + b, 0)
}
}
},
queries: {
// Sequential chain: double the numbers, then sum them
result: [
['math', 'double', {values: '$.given.data'}],
['math', 'sum', {values: '@'}] // @ refers to return of 'math:double'
]
}
})
console.log(result.result) // 30 (sum of [2, 4, 6, 8, 10])
A service can be passed as an argument to another service. Services that support other services as arguments have arg defined with {type: 'service'}
.
util.map._argtypes = {
service: {type: 'service'},
}
This means the argument is expecting a service definition. The child service (stringLib:toUppercase
in this case) will have access to '@' context - this is how it accesses the iteration value.
uppercase: ['util', 'map', {
on: '$.filtered',
service: ['stringLib', 'toUppercase', {on: '@'}]
}],
Remember: {type: 'service'}
declared on the service is MicroQL's cue to compile the argument as a service descriptor. Otherwise the same argument will be interpreted as an array with some strings and an object in it - no compilation and no service lookup.
The syntax used here can be further condensed; read below for method syntax.
MicroQL supports 'infix' notation where the first argument appears on the left.
['$.filtered', 'util:map', {service: ['@', 'stringLib:toUppercase']}],
Services that support method syntax have an arg defined with {argOrder: 0}
. By convention we typically use on
for {argOrder: 0}
:
util.map._argtypes = {
on: {argOrder: 0}
}
Method syntax is transformed into the equivalent service definitions, and then compiled and executed in the same way. So it's purely for user convenience and has the same semantics.
// method syntax:
['$.filtered', 'util:map', {service: ['@', 'stringLib:toUppercase']}],
// is transformed into standard service calls
['util', 'map', {
on: '$.filtered',
service: ['stringLib', 'toUppercase', {on: '@'}]}],
// and then the service lookups are performed and these become functions which get called by the execution engine
- Declarative Query Language: Express complex service orchestration with simple JSON
- Automatic Dependency Resolution: Infers execution order from data dependencies
- Parallel Execution: Runs independent operations concurrently for optimal performance
- Context Chaining: Pass data between services using
@
and$
references - Method Syntax: Concise
service:method
notation for transformations - Sequential Chains: Execute multi-step workflows with automatic data flow
- Validation System: Built-in Zod-based validation for inputs and outputs
- Error Handling: Comprehensive error recovery with
onError
chains - Retry & Timeout: Built-in resilience patterns for unreliable services
- Circular Dependency Detection: Prevents infinite loops at compile time
precheck
and postcheck
validations can be created at both the service and query level. See Validation Guide for details.
const userService = {
async createUser(args) {
return {id: generateId(), ...args.userData}
}
}
// service level validation
userService.createUser._validators = {
precheck: {
userData: {
name: ['string'],
email: ['string', 'email'],
age: ['number', 'positive']
}
},
postcheck: {
id: ['string', 'uuid'],
name: ['string'],
email: ['string', 'email']
}
}
All service calls support an onError
handler, and optional ignoreErrors: true
. Error handlers are just regular services. The builtin util
service has a print
action that can be useful for this.
const queries = {
user: ['users', 'getUser', {
id: '$.given.userId',
onError: ['@', 'util:print'],
ignoreErrors: true,
}]
}
MicroQL includes utility functions for common data operations:
import {util} from 'microql/services'
const queries = {
// Filter, map, reduce operations
filtered: ['util', 'filter', {on: '$.data', service: ['data', 'isActive', {item: '@'}]}],
mapped: ['util', 'map', {on: '$.filtered', service: ['data', 'getName', {item: '@'}]}],
// Conditional logic
result: ['util', 'when', {
condition: '$.user.isAdmin',
then: ['admin', 'getAdminData'],
else: ['user', 'getRegularData']
}]
}
Configure global behavior:
const result = await query({
settings: {
debug: true, // Enable debug logging
timeout: 30000, // Set a default timeout (30s)
retry: 2, // Set a default retry count
onError: ['@', 'util:print'] // Set a global error handler
},
// ... services and queries
})
- Service Development Guide - How to write and structure services
- Validation System - Type validation and contracts
npm install microql
// Orchestrate multiple API calls with dependency resolution
const result = await query({
given: {customerId: 'cust_123'},
services: {api, cache, notifications},
queries: {
// These run in parallel since they're independent
customer: ['api', 'getCustomer', {id: '$.given.customerId'}],
preferences: ['api', 'getPreferences', {id: '$.given.customerId'}],
// This waits for customer data
orders: ['api', 'getOrders', {customerId: '$.customer.id'}],
// Cache the complete customer profile
profile: ['cache', 'store', {
key: 'profile_$.customer.id',
data: {
customer: '$.customer',
preferences: '$.preferences',
recentOrders: '$.orders'
}
}],
// Send notification after everything is cached
notification: ['notifications', 'send', {
to: '$.customer.email',
template: 'welcome',
data: '$.profile'
}]
}
})
// Transform and validate data through multiple steps
const result = await query({
given: {csvData: "name,age,email\nJohn,25,[email protected]\n..."},
services: {parser, validator, transformer, storage},
queries: {
// Chain: parse → validate → transform → store
// uses method syntax to implicitly pass to {argOrder: 0}
processed: [
['$.given.csvData', 'parser:parseCsv'],
['@', 'validator:validateRows'],
['@', 'transformer:enrichData'],
['@', 'storage:bulkInsert']
]
}
})
npm test
GraphQL is primarily developed for field extraction, whereas MicroQL was designed for service orchestration. You can do service orchestration with GraphQL but it requires that you put a lot more leg work into resolvers and that you manually think about dependency resolution. MicroQL infers dependency resolution and parallelism from the paths that are referenced. MicroQL also doesn't invent a completely new syntax - it's mostly a subset of JSON. We found ways to do that without being pedantic and verbose.
The MicroQL codebase is also small (<1000 lines), dependencies lodash
and zod
.
See here for a more detailed comparison.
I think the problem that most people are trying to solve with TypeScript would be better solved with input validations. That is: you can type for internal consistency all you like, but as soon as you incorporate something outside your domain, your static type analysis fails. MicroQL is literally for composing things outside of your domain.
Secondly, Javascript compilation layers became a Tower of Babel over ten years ago. Your main tool is literally called babel
. Do you really need all that? Maybe all you need is JSON. Try it out.
It's faster than n8n, slower than native Javascript. Performance optimization should be considered in its infancy - particularly I have not tried this with any long running processes to ensure that there are no memory leaks.
MIT
MicroQL - Microservice orchestration made simple.