@mayademcom/nestjs-hasura
v1.0.0
Published
Hasura module for NestJs projects.
Readme
@mayademcom/nestjs-hasura
A powerful and flexible NestJS module for seamless integration with Hasura GraphQL Engine.
Features
✨ Fluent Builder API - Chain methods for clean and readable code
🔐 Built-in Authentication - Admin secret and JWT token support
📁 GraphQL File Loading - Load queries from .gql files with caching
🎯 TypeScript First - Full type safety and IntelliSense support
🚀 Flexible Configuration - Sync and async module registration
🎨 Custom Decorators - Extract auth tokens from request headers
⚡ Performance Optimized - Query caching for improved performance
🧪 Well Tested - Comprehensive unit and integration tests
Installation
npm install @mayademcom/nestjs-hasura graphql-requestQuick Start
1. Import the Module
Synchronous Configuration:
import { Module } from '@nestjs/common';
import { HasuraModule } from '@mayademcom/nestjs-hasura';
@Module({
imports: [
HasuraModule.forRoot({
endpoint: 'https://your-hasura.app/v1/graphql',
adminSecret: 'your-admin-secret', // Optional
}),
],
})
export class AppModule {}Asynchronous Configuration (Recommended for Production):
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { HasuraModule } from '@mayademcom/nestjs-hasura';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
HasuraModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
endpoint: configService.get<string>('HASURA_ENDPOINT'),
adminSecret: configService.get<string>('HASURA_ADMIN_SECRET'),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}2. Use in Your Service
import { Injectable } from '@nestjs/common';
import { HasuraService } from '@mayademcom/nestjs-hasura';
@Injectable()
export class UserService {
constructor(private readonly hasura: HasuraService) {}
async getUsers(limit: number) {
const query = `
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
name
email
}
}
`;
return this.hasura
.requestBuilder()
.withAdminSecret()
.withQuery(query)
.withVariables({ limit })
.execute();
}
}Usage Guide
Request Builder API
The module provides a fluent builder API for constructing GraphQL requests:
Basic Query
const result = await this.hasura
.requestBuilder()
.withQuery('query { users { id name } }')
.execute();With Variables
const result = await this.hasura
.requestBuilder()
.withQuery('query GetUser($id: uuid!) { users_by_pk(id: $id) { id name } }')
.withVariables({ id: 'user-id' })
.execute();With Admin Secret
const result = await this.hasura
.requestBuilder()
.withAdminSecret()
.withQuery('query { users { id } }')
.execute();With Authorization Token
const result = await this.hasura
.requestBuilder()
.withAuthorizationToken(jwtToken)
.withQuery('query { me { id name } }')
.execute();With Custom Headers
const result = await this.hasura
.requestBuilder()
.withHeaders({
'x-hasura-role': 'user',
'x-hasura-user-id': '123',
})
.withQuery('query { users { id } }')
.execute();Chaining Multiple Methods
const result = await this.hasura
.requestBuilder()
.withAdminSecret()
.withAuthorizationToken(token)
.withHeaders({ 'x-tenant-id': 'tenant-123' })
.withQuery(query)
.withVariables(variables)
.execute();Loading Queries from Files
Store your GraphQL queries in .gql files for better organization:
Create a query file:
# src/queries/get-users.gql
query GetUsers($limit: Int!) {
users(limit: $limit) {
id
name
email
created_at
}
}Use in your service:
@Injectable()
export class UserService {
constructor(private readonly hasura: HasuraService) {}
async getUsers(limit: number) {
return this.hasura
.requestBuilder()
.withAdminSecret()
.withQueryFromFile('src/queries/get-users.gql')
.withVariables({ limit })
.execute();
}
}Benefits:
- ✅ Queries are cached automatically after first load
- ✅ Better syntax highlighting in IDE
- ✅ Easier to maintain and version control
- ✅ Separation of concerns
Authorization Decorator
Extract authorization tokens from request headers:
import { Controller, Post, Body } from '@nestjs/common';
import { Authorization, HasuraService } from '@mayademcom/nestjs-hasura';
@Controller('webhooks')
export class WebhookController {
constructor(private readonly hasura: HasuraService) {}
// Optional token
@Post('public')
async publicWebhook(
@Authorization() token: string | null,
@Body() payload: any,
) {
if (token) {
// Authenticated request
return this.hasura
.requestBuilder()
.withAuthorizationToken(token)
.withQuery('query { me { id } }')
.execute();
}
// Public request
return { message: 'Public endpoint' };
}
// Required token
@Post('protected')
async protectedWebhook(
@Authorization({ required: true }) token: string,
@Body() payload: any,
) {
return this.hasura
.requestBuilder()
.withAuthorizationToken(token)
.withQuery('query { me { id name } }')
.execute();
}
// Custom prefix
@Post('api-key')
async apiKeyWebhook(
@Authorization({ prefix: 'ApiKey' }) apiKey: string | null,
@Body() payload: any,
) {
// Header: "ApiKey xxx-yyy-zzz"
return { apiKey };
}
}Decorator Options:
| Option | Type | Default | Description |
| ---------- | ------- | ---------- | -------------------------------------------------- |
| required | boolean | false | Throws UnauthorizedException if token is missing |
| prefix | string | 'Bearer' | Token prefix to strip from header |
Module Configuration
Module Registration Methods
The module provides four registration methods:
forRoot(config)
Global module with synchronous configuration:
HasuraModule.forRoot({
endpoint: 'https://hasura.app/v1/graphql',
adminSecret: 'secret',
});forRootAsync(config)
Global module with asynchronous configuration:
HasuraModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
endpoint: config.get('HASURA_ENDPOINT'),
adminSecret: config.get('HASURA_ADMIN_SECRET'),
}),
inject: [ConfigService],
});register(config)
Module-scoped with synchronous configuration:
HasuraModule.register({
endpoint: 'https://hasura.app/v1/graphql',
});registerAsync(config)
Module-scoped with asynchronous configuration:
HasuraModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
endpoint: config.get('HASURA_ENDPOINT'),
}),
inject: [ConfigService],
});Configuration Options
interface HasuraConfig {
endpoint: string; // Hasura GraphQL endpoint URL
adminSecret?: string; // Optional admin secret for admin operations
}Advanced Usage
Multiple Hasura Instances
Use module-scoped registration for different Hasura endpoints:
// user.module.ts
@Module({
imports: [
HasuraModule.register({
endpoint: 'https://users-hasura.app/v1/graphql',
adminSecret: 'users-secret',
}),
],
providers: [UserService],
})
export class UserModule {}
// analytics.module.ts
@Module({
imports: [
HasuraModule.register({
endpoint: 'https://analytics-hasura.app/v1/graphql',
adminSecret: 'analytics-secret',
}),
],
providers: [AnalyticsService],
})
export class AnalyticsModule {}Direct GraphQL Client Access
The HasuraService extends GraphQLClient from graphql-request, so you can use it directly:
// Using the native GraphQLClient request method
const result = await this.hasura.request(query, variables, headers);TypeScript Type Safety
Define your GraphQL response types:
interface User {
id: string;
name: string;
email: string;
}
interface GetUsersResponse {
users: User[];
}
const result = await this.hasura
.requestBuilder()
.withQuery(query)
.execute<GetUsersResponse>();
// result.users is now typed as User[]Environment Variables
Create a .env file in your project root:
HASURA_ENDPOINT=https://your-hasura.app/v1/graphql
HASURA_ADMIN_SECRET=your-admin-secretAPI Reference
HasuraService
requestBuilder(): HasuraRequestBuilder
Creates a new request builder instance.
HasuraRequestBuilder
withQuery(query: string): HasuraRequestBuilder
Sets the GraphQL query string.
withQueryFromFile(filePath: string): HasuraRequestBuilder
Loads and sets the GraphQL query from a .gql file.
withVariables(variables: Variables): HasuraRequestBuilder
Sets the GraphQL query variables.
withAdminSecret(): HasuraRequestBuilder
Includes the admin secret header from module configuration.
withAuthorizationToken(token: string): HasuraRequestBuilder
Adds Bearer token authorization header.
withHeaders(headers: Record<string, string>): HasuraRequestBuilder
Adds custom headers to the request.
execute<T>(): Promise<T>
Executes the GraphQL request and returns the response.
Authorization Decorator
@Authorization(options?: AuthorizationOptions)
Extracts and validates authorization token from request headers.
Options:
required?: boolean- Throws error if token is missing (default:false)prefix?: string- Token prefix to remove (default:'Bearer')
Best Practices
1. Use Environment Variables
Never hardcode credentials:
HasuraModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
endpoint: config.get('HASURA_ENDPOINT'),
adminSecret: config.get('HASURA_ADMIN_SECRET'),
}),
inject: [ConfigService],
});2. Organize Queries in Files
Keep your queries in .gql files:
src/
├── queries/
│ ├── users/
│ │ ├── get-users.gql
│ │ ├── create-user.gql
│ │ └── update-user.gql
│ └── posts/
│ ├── get-posts.gql
│ └── create-post.gql3. Use Type Safety
Define response types for better IDE support:
interface GetUsersResponse {
users: Array<{
id: string;
name: string;
email: string;
}>;
}
const result = await hasura
.requestBuilder()
.withQuery(query)
.execute<GetUsersResponse>();4. Handle Errors Properly
try {
const result = await this.hasura
.requestBuilder()
.withAdminSecret()
.withQuery(query)
.execute();
return result;
} catch (error) {
this.logger.error('Hasura query failed', error);
throw new InternalServerErrorException('Failed to fetch data');
}How to Release
This package uses release-it for automated releases. To create a new release:
Prerequisites
- Ensure you're on the
mainbranch - All tests must pass
- Code quality checks must pass
- You have npm publish permissions
Release Process
# 1. Pull latest changes
git checkout main
git pull origin main
# 2. Run release command (automated versioning)
npm run releaseThe npm run release command will:
- Run the tests
- Analyze commits since last release using Conventional Commits
- Determine version bump automatically (major/minor/patch)
- Update
package.jsonversion - Generate/update
CHANGELOG.md - Create git commit and tag
- Push changes and tags to GitHub
- Publish to npm registry
Manual Version Specification
If you need to manually specify the version:
# Patch release (1.0.0 -> 1.0.1)
npm run release -- patch
# Minor release (1.0.0 -> 1.1.0)
npm run release -- minor
# Major release (1.0.0 -> 2.0.0)
npm run release -- major
# Specific version
npm run release -- 1.2.3
# Pre-release versions
npm run release -- prepatch --preRelease=beta # 1.0.0 -> 1.0.1-beta.0
npm run release -- preminor --preRelease=alpha # 1.0.0 -> 1.1.0-alpha.0Dry Run
Test the release process without actually releasing:
npm run release -- --dry-runThis will show you what would happen without making any changes.
Commit Message Convention
Follow Conventional Commits for automatic versioning.
Troubleshooting
Authentication Issues:
# Login to npm
npm login
# Verify authentication
npm whoamiGit Issues:
# Ensure working directory is clean
git status
# Ensure you're on main branch
git checkout main
# Pull latest changes
git pull origin mainDry Run First:
# Always test first
npm run release -- --dry-runThis will show you exactly what will happen without making any changes.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Development Setup
# Clone the repository
git clone https://github.com/mayademcom/nestjs-hasura.git
cd nestjs-hasura
# Install dependencies
npm install
# Run tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:cov
# Build the package
npm run build
# Lint code
npm run lintRunning Tests
# All tests
npm test
# Unit tests only
npm run test:unit
# Integration tests only
npm run test:integration
# Watch mode
npm run test:watch
# Coverage report
npm run test:covLicense
MIT © Mayadem
Support
Changelog
See CHANGELOG.md for release history.
Made with ❤️ by Mayadem
