🌊 Squall#
A type-safe isomorphic GraphQL client generator for Gleam.
Squall parses .gql files in your project, introspects your GraphQL endpoint, and generates fully type-safe Gleam code that works on both Erlang and JavaScript targets. Write your queries once, run them anywhere!
⚠️ Warning: This project is in early development and may contain bugs. Also hasn't been published yet.
Features#
✨ Isomorphic - Works on Erlang (backend) and JavaScript (browser/Node.js)
- Type-safe code generation from GraphQL schema
- Convention over configuration -
.gqlfiles insrc/**/graphql/directories - Schema introspection from GraphQL endpoints
- Supports queries, mutations, and subscriptions
- Target-specific HTTP adapters (gleam_httpc for Erlang, Fetch API for JavaScript)
- Client abstraction with authentication and custom headers
Installation#
Add squall to your gleam.toml:
[dependencies]
squall = "0.1.0"
# If using JavaScript target, also add:
gleam_javascript = ">= 0.3.0 and < 2.0.0"
The gleam_javascript dependency provides Promise support needed for async operations on the JavaScript target.
Quick Start#
1. Create a GraphQL query file#
Create a file at src/my_app/graphql/get_character.gql:
query GetCharacter($id: ID!) {
character(id: $id) {
id
name
status
species
}
}
2. Generate type-safe code#
gleam run -m squall generate https://rickandmortyapi.com/graphql
3. Use the generated code#
import squall
import my_app/graphql/get_character
pub fn main() {
// Create a client for your target
// On Erlang:
let client = squall.new_erlang_client(
"https://rickandmortyapi.com/graphql",
[]
)
// On JavaScript:
// let client = squall.new_javascript_client(
// "https://rickandmortyapi.com/graphql",
// []
// )
// Use the generated function - works on both targets!
case get_character.get_character(client, "1") {
Ok(response) -> {
io.println("Character: " <> response.character.name)
}
Error(err) -> {
io.println("Error: " <> err)
}
}
}
Note: On JavaScript, the generated function returns a Promise(Result(...)) instead of Result(...). See the examples for how to handle both targets.
Creating a Client#
Squall provides target-specific client constructors:
Erlang Target#
import squall
// Basic client
let client = squall.new_erlang_client(
"https://api.example.com/graphql",
[#("X-Custom-Header", "value")]
)
// With bearer token authentication
let client = squall.new_erlang_client_with_auth(
"https://api.example.com/graphql",
"your-api-token-here"
)
JavaScript Target#
import squall
// Basic client
let client = squall.new_javascript_client(
"https://api.example.com/graphql",
[#("X-Custom-Header", "value")]
)
// With bearer token authentication
let client = squall.new_javascript_client_with_auth(
"https://api.example.com/graphql",
"your-api-token-here"
)
Isomorphic Code#
For code that works on both targets, use @target() conditionals:
import squall
@target(erlang)
fn create_client() -> squall.Client {
squall.new_erlang_client("https://api.example.com/graphql", [])
}
@target(javascript)
fn create_client() -> squall.Client {
squall.new_javascript_client("https://api.example.com/graphql", [])
}
pub fn main() {
let client = create_client() // Works on both targets!
// ... use the client
}
Isomorphic Architecture#
Squall's isomorphic design means you write your GraphQL queries once, and they work on both Erlang and JavaScript:
What's Different Between Targets?#
| Aspect | Erlang | JavaScript |
|---|---|---|
| HTTP Client | gleam_httpc |
Fetch API |
| Return Type | Result(T, String) |
Promise(Result(T, String)) |
| Execution | Synchronous | Asynchronous |
| Client Constructor | new_erlang_client() |
new_javascript_client() |
What's the Same?#
✅ GraphQL queries - Same .gql files
✅ Generated types - Identical response types
✅ Generated decoders - Same JSON parsing logic
✅ Business logic - Share response handling code
✅ Generated functions - Same function signatures (except return type)
The only code you need to write differently is:
- Client creation (use
@target()conditionals) - Promise handling on JavaScript (use
promise.await())
Everything else is truly isomorphic!
How It Works#
Squall follows a simple workflow:
- Discovery: Finds all
.gqlfiles insrc/**/graphql/directories - Parsing: Parses GraphQL operations (queries, mutations, subscriptions)
- Introspection: Fetches the GraphQL schema from your endpoint
- Type Mapping: Maps GraphQL types to Gleam types
- Code Generation: Generates:
- Custom types for responses
- JSON decoders
- Type-safe functions with proper parameters
Project Structure#
your-project/
├── src/
│ └── my_app/
│ └── graphql/
│ ├── get_user.gql # Your GraphQL query
│ └── get_user.gleam # Generated code
└── gleam.toml
Examples#
Check out the examples directory for complete working examples:
- 01-erlang - Erlang/OTP backend example
- 02-javascript - JavaScript (Node.js/Browser) example
- 03-isomorphic - Cross-platform example that runs on both!
Generated Code#
For a query like:
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
Squall generates type-safe Gleam code with:
Response Types:
pub type GetUserResponse {
GetUserResponse(user: Option(User))
}
pub type User {
User(
id: String,
name: Option(String),
email: Option(String)
)
}
Decoders:
fn get_user_response_decoder() -> decode.Decoder(GetUserResponse) {
// Auto-generated JSON decoder
}
Query Function:
// On Erlang - returns Result
pub fn get_user(
client: squall.Client,
id: String,
) -> Result(GetUserResponse, String) {
squall.execute_query(client, query, variables, decoder)
}
// On JavaScript - returns Promise(Result)
pub fn get_user(
client: squall.Client,
id: String,
) -> Promise(Result(GetUserResponse, String)) {
squall.execute_query(client, query, variables, decoder)
}
Type Mapping#
| GraphQL Type | Gleam Type |
|---|---|
String |
String |
Int |
Int |
Float |
Float |
Boolean |
Bool |
ID |
String |
[Type] |
List(Type) |
Type (nullable) |
Option(Type) |
Type! (non-null) |
Type |
| Custom Objects | Custom Gleam types |
CLI Commands#
Generate Code#
# With explicit endpoint
gleam run -m squall generate https://api.example.com/graphql
# Using environment variable
export GRAPHQL_ENDPOINT=https://api.example.com/graphql
gleam run -m squall generate
Architecture#
Squall is built with a modular, isomorphic architecture:
Core Modules#
discovery: Finds and reads.gqlfilesparser: Lexes and parses GraphQL operationsschema: Introspects and parses GraphQL schemastype_mapping: Maps GraphQL types to Gleam typescodegen: Generates platform-agnostic Gleam codeerror: Comprehensive error handling
HTTP Adapter Pattern#
adapter: Defines the HTTP adapter interfaceadapter/erlang: Erlang implementation usinggleam_httpcadapter/javascript: JavaScript implementation using Fetch API
The generated code calls squall.execute_query(), which automatically uses the correct HTTP adapter for your target platform.
Development#
Running Tests#
gleam test
Project Structure#
squall/
├── src/
│ ├── squall.gleam # CLI entry point & execute_query
│ └── squall/
│ ├── adapter.gleam # HTTP adapter interface
│ ├── adapter/
│ │ ├── erlang.gleam # Erlang HTTP adapter
│ │ └── javascript.gleam # JavaScript HTTP adapter
│ └── internal/
│ ├── discovery.gleam # File discovery
│ ├── parser.gleam # GraphQL parser
│ ├── schema.gleam # Schema introspection
│ ├── type_mapping.gleam # Type conversion
│ ├── codegen.gleam # Code generation
│ └── error.gleam # Error types
├── test/
│ ├── discovery_test.gleam
│ ├── parser_test.gleam
│ ├── schema_test.gleam
│ ├── type_mapping_test.gleam
│ └── codegen_test.gleam
├── birdie_snapshots/ # Snapshot tests
└── examples/ # Working examples
├── 01-erlang/ # Erlang example
├── 02-javascript/ # JavaScript example
└── 03-isomorphic/ # Cross-platform example
Roadmap#
Completed ✅#
- Query support
- Mutation support
- Subscription support
- Type-safe code generation
- Schema introspection
- Client abstraction with headers/authentication
- Isomorphic support (Erlang + JavaScript targets)
- HTTP adapter pattern
- Target-conditional compilation
In Progress 🚧#
- Fragment support
- Directive handling (
@include,@skip, etc.) - Custom scalar type mapping
- Schema caching for faster regeneration
Contributing#
Contributions are welcome! Please:
- Follow TDD principles
- Add tests for new features
- Update snapshots when changing code generation
- Follow Gleam style guidelines
License#
Apache-2.0