A code-first GraphQL + Microservice library for NestJS, designed for small to medium systems with full entity-centric automation and opinionated relationship management. This library streamlines the development of microservices with GraphQL integration, focusing on automation and clarity in relationship handling.
Write your entities once, get complete GraphQL APIs, microservice controllers, and database operations automatically.
The design is guided by the Asymmetric Relation Ownership Pattern (AROP), introduced below in the Design Philosophy section.
⚠️ Note: This is a personal project, primarily a proof-of-concept for learning and experimentation. It also showcases automation patterns and a working microservice architecture.
- Decorator-Driven Development:
@AutoField,@AutoObjectType,@AutoRelationdecorators for metadata-driven entity definitions - View Policies: Field-level access control (
create,read,update,hidden) for precise API control - Code-first Entity Definition: Define your entity once, use it across ORM, Microservice APIs, and GraphQL
- Automatic CRUD and Relationship Handling: Factory-based generation for services, controllers, and resolvers
- GraphQL Resolver Generation: Automatic ObjectType, InputType creation, and resolver wiring from entity metadata
- Advanced Querying: Nested AND/OR filtering, offset pagination, dataloader-backed relation queries
- Relationship Ownership: Asymmetric Relation Ownership Pattern (AROP) for clear relationship management
- Type-Safe APIs: Generated GraphQL types from entity metadata with full TypeScript support
- DataLoader Integration: Automatic N+1 query prevention for relations
- Docker-ready: Quickstart with GraphQL Gateway + RabbitMQ + MySQL + Sample Cat Management Service
Get up and running quickly using Docker Compose with the following steps:
git clone https://github.com/vovanphu/nestjs-graphx
cd nestjs-graphx
yarn
yarn docker-up:dev -dAccess the GraphQL playground at http://localhost:4000 to interact with the API.
Explore the prebuilt services:
cat-management: Manages Cat Boundary Context.graphql-gateway: API Gateway.
Note: For development convenience, local folders (including node_modules) are bind-mounted into containers. This approach simplifies debugging but is not recommended for production, especially if using native bindings (e.g.,
argon2). Consider using a clean container setup for production environments.Troubleshooting: If you encounter issues with Docker Compose, ensure Docker and Docker Compose are installed and running. Check for port conflicts (e.g., port 4000 or 3306) and free them if necessary. Logs can be viewed with
docker-compose logsfor debugging.
For developers who prefer not to use Docker, you can set up the project manually:
-
Clone the Repository:
git clone https://github.com/vovanphu/nestjs-graphx cd nestjs-graphx -
Install Dependencies:
yarn
-
Environment Configuration:
- Copy
.env.development.localto.envand adjust the variables as needed for your local setup (e.g., database credentials).
- Copy
-
Database Setup:
- Set up a MySQL database according to the configuration in
.env. - Ensure the database is running and accessible.
- Set up a MySQL database according to the configuration in
-
Run Services:
- Start the
cat-managementservice:yarn start:dev cat-management
- Start the
graphql-gatewayservice in another terminal:yarn start:dev graphql-gateway
- Start the
-
Access the GraphQL playground at http://localhost:4000.
- Framework: NestJS
- Database: TypeORM (MySQL)
- API Layer: GraphQL (Code-first)
- Transport Layer: RabbitMQ (Microservices)
- Containerization: Docker Compose
This comprehensive guide covers all aspects of using NestJS-GraphX, from basic entity definition to advanced relationship management and GraphQL API generation.
The core of NestJS-GraphX is its decorator-driven entity system. Define your entities once and automatically generate GraphQL APIs, microservice controllers, and database operations.
import {
AutoField,
AutoObjectType,
AutoRelation,
BaseEntityObject,
} from '@app/graphx';
import { Column, Entity, OneToMany, OneToOne } from 'typeorm';
@AutoObjectType()
@Entity()
export class User extends BaseEntityObject<User> {
@AutoField({ typeFn: () => String, viewPolicy: ['create', 'read'] })
@Column({ unique: true })
username: string;
@AutoField({ typeFn: () => String, viewPolicy: ['create', 'update'] })
@Column()
password: string;
@AutoField({ typeFn: () => String })
@Column()
email: string;
@AutoRelation({
target: () => UserProfile,
type: 'one-to-one',
inverseSide: (profile) => profile.user,
})
@OneToOne(() => UserProfile, (profile) => profile.user)
profile: UserProfile;
@AutoRelation({
target: () => Post,
type: 'one-to-many',
inverseSide: (post) => post.author,
})
@OneToMany(() => Post, (post) => post.author)
posts: Post[];
}@AutoObjectType()
Marks a class as a GraphQL Object Type and enables automatic schema generation.
@AutoField(options)
Defines GraphQL fields with precise control:
@AutoField({
typeFn: () => String, // GraphQL type
viewPolicy: ['create', 'read', 'update'], // API access control
fieldOptions: { nullable: true } // Additional GraphQL options
})View Policies:
'create'- Field appears in create mutations'read'- Field appears in queries/resolvers'update'- Field appears in update mutations'hidden'- Field is never exposed in API
@AutoRelation(options)
Defines relationships with ownership semantics:
@AutoRelation({
target: () => RelatedEntity, // Target entity class
type: 'one-to-one' | 'one-to-many' | 'many-to-one' | 'many-to-many',
owner: true, // Who controls the relationship
inverseSide: (entity) => entity.field, // Inverse relationship accessor
})import { TypeOrmCrudServiceFactory } from '@app/graphx';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService extends TypeOrmCrudServiceFactory(User) {}import { MicroserviceControllerFactory } from '@app/graphx';
import { Controller } from '@nestjs/common';
@Controller()
export class UserController extends MicroserviceControllerFactory(
User,
UserService,
) {}import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [TypeOrmModule.forFeature([User])],
providers: [UserService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}import { CrudProxyServiceFactory } from '@app/graphx';
import { Injectable } from '@nestjs/common';
@Injectable()
export class UserService extends CrudProxyServiceFactory(
User,
'USER_SERVICE', // RabbitMQ client token
) {}import { GraphqlCrudResolverFactory } from '@app/graphx';
import { Resolver } from '@nestjs/graphql';
@Resolver(User)
export class UserResolver extends GraphqlCrudResolverFactory(User, {
providers: [UserService, UserProfileService, PostService],
}) {}import { Module } from '@nestjs/common';
import { GraphQLModule } from '@nestjs/graphql';
import { ClientProxyFactory, Transport } from '@nestjs/microservices';
@Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: true,
playground: true,
context: ({ req, res }) => ({ req, res, dataloader: new Map() }),
}),
],
providers: [
UserResolver,
{
provide: 'USER_SERVICE',
useFactory: () => ClientProxyFactory.create({
transport: Transport.RMQ,
options: {
urls: [process.env.RMQ_URL],
queue: 'user_service_queue',
},
}),
},
],
})
export class AppModule {}NestJS-GraphX automatically generates complete GraphQL APIs from your entity definitions.
# Single entity operations
findUser(record: FindUserInput): UserResult
findUserById(userId: ID): UserResult
# Multiple entities with advanced filtering
findManyUsers(options: FindManyUsersOptions): UserManyResult
# Relationship queries (DataLoader-powered)
query GetUserWithRelations {
findUserById(userId: "123") {
entity {
id
username
email
profile {
firstName
lastName
}
posts(options: {
filter: { title: { like: "GraphQL%" } }
pagination: { offset: { page: 1, limit: 10 } }
}) {
entities {
id
title
content
}
total
page
limit
}
}
}
}# CRUD operations
createUser(record: CreateUserInput): UserResult
updateUser(record: UpdateUserInput): UserResult
deleteUser(userId: ID): UserResult
softDeleteUser(userId: ID): UserResult
# Relationship operations
setProfileForUser(userId: ID, profileId: ID): UserResult
addPostForUser(userId: ID, postId: ID): UserResult
removePostFromUser(userId: ID, postId: ID): UserResult
# Example mutation
mutation CreateUser {
createUser(record: {
username: "johndoe"
password: "securepass"
email: "john@example.com"
}) {
entity {
id
username
email
}
}
}NestJS-GraphX provides powerful filtering capabilities with nested AND/OR conditions:
// Complex filter example
const filterOptions = {
and: [
{ username: { like: 'john%' } },
{ email: { notLike: '%test%' } },
{
or: [
{ createdAt: { greater: '2023-01-01' } },
{ profile: { firstName: { equal: 'John' } } }
]
}
]
};
// GraphQL usage
query FindUsers {
findManyUsers(options: {
filter: {
and: [
{ username: { like: "john%" } },
{ or: [
{ createdAt: { greater: "2023-01-01" } },
{ profile: { firstName: { equal: "John" } } }
]}
]
}
sort: { field: "createdAt", direction: DESC }
pagination: { offset: { page: 1, limit: 20 } }
}) {
entities { id username email }
total
pages
page
limit
}
}String Fields:
equal,like,notLike,empty,notEmpty,in,notIn
Number/Date Fields:
equal,greater,greaterOrEqual,lesser,lesserOrEqual,empty,notEmpty,in,notIn
Boolean Fields:
is,isNot
NestJS-GraphX enforces the Asymmetric Relation Ownership Pattern for clear relationship semantics.
One-to-One:
@AutoRelation({
target: () => UserProfile,
type: 'one-to-one',
owner: true, // Profile owns the relationship
})
profile: UserProfile;One-to-Many:
@AutoRelation({
target: () => Post,
type: 'one-to-many',
owner: true, // User owns the relationship
})
posts: Post[];Many-to-Many:
@AutoRelation({
target: () => Group,
type: 'many-to-many',
owner: true, // User owns the relationship
})
@ManyToMany(() => Group)
@JoinTable() // Creates join table automatically
groups: Group[];- Owner Side: Controls mutations, holds foreign keys, exposes mutation APIs
- Inverse Side: Read-only through DataLoader, no mutation APIs
- Database: Foreign keys live on owner side
- API: Only owner service exposes relationship mutations
# Database Configuration
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=user
DB_PASSWORD=password
DB_DATABASE=your_database
# RabbitMQ Configuration
RMQ_URL=amqp://localhost:5672
USER_SERVICE_RMQ_QUEUE=user_service_queue
# GraphQL Configuration
GRAPHQL_PORT=4000
NODE_ENV=developmentTypeOrmModule.forRootAsync({
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST'),
port: +configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
synchronize: configService.get('NODE_ENV') === 'development',
autoLoadEntities: true,
}),
inject: [ConfigService],
})Explore the complete working example in the codebase:
- Entity Definitions:
libs/entities/src/cat/ - Microservice Implementation:
apps/cat-management/src/cat/ - GraphQL Gateway:
apps/graphql-gateway/src/cat/
The Cat management example demonstrates:
- Entity relationships (Cat → Profile, Addresses, Groups)
- Microservice communication via RabbitMQ
- GraphQL API generation with DataLoader
- Advanced filtering and pagination
In distributed systems, especially microservice architectures with independent persistent layers, modeling relationships between entities can quickly become ambiguous and messy.
- Where should relation data be stored?
- Which service is responsible for updating it?
- What happens if multiple services try to change it?
The Asymmetric Relation Ownership Pattern offers a simple yet consistent answer:
Only one side of a relationship should be allowed to own and mutate it - the side that is more semantically dependent.
In any business-domain relationship between two entities:
- The entity more dependent on the relationship should be the owning side.
- The owning side should:
- Hold the foreign key or pivot metadata in storage.
- Totally control any associated command logic (e.g., relation creation, deletion).
- The non-owning side:
- May read the relationship via queries (mostly through gateway orchestration).
- May acknowledge information about the relationship update via events (in choreography pattern).
- Must not perform any mutations related to that relationship.
This pattern is enforced across all layers:
- Database: Join columns live on the owner side.
- Service: Only the owner's service exposes mutation APIs.
- Gateway: Only proxies commands to the owner's service, others are read-only.
- Reduce Cross-Service Coupling: Only the service that understands the relation's business meaning can mutate it.
- Reduce the Need for Distributed Transactions: No need for orchestrator or two-phase commits for relation mutations.
- Clarifies API Ownership: Easy to reason about who exposes what and why.
- Enables Better Tooling: Relation ownership can be inferred, used to auto-generate resolvers/controllers/service schemas correctly.
| Relationship | Owner | Why |
|---|---|---|
UserProfile – User (1–1) |
UserProfile |
Exists only in the context of a User |
Task – User (N–N) |
Task (TaskAssignee in TaskManagementService) |
A Task depends on who is assigned, not vice versa |
Address – Order (1–N) |
Address |
Address exists solely under an Order |
Note: AROP is not a widely adopted standard or formal pattern — it's simply a naming and enforcement convention used in this project to reduce ambiguity and guide automation logic.
- Short-term:
- Add optional support for monolithic architecture (non-microservice mode).
- Enhance documentation with more examples and tutorials.
- Mid-term:
- Support for additional databases (e.g., NoSQL like MongoDB).
- Integration with REST API alongside GraphQL.
- Long-term:
- Implement Role-Based Access Control (RBAC) for security.
- Explore scalability improvements for larger systems.
- gRPC: Not supported due to incompatibility with the code-first philosophy.
- Scale: Best suited for experimentation, learning, and small to medium-sized products. Not designed for large-scale, high-load production systems.
- Availability: Not published on npm (yet), use via local path or internal git repository.
This project is licensed under the MIT License - see the LICENSE file for details.