Skip to content

About

Code-first GraphQL + Microservice toolkit for NestJS, with opinionated relationship modeling.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Repository files navigation

NestJS-GraphX

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.

Features

  • Decorator-Driven Development: @AutoField, @AutoObjectType, @AutoRelation decorators 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

Quick Start with Docker Compose

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 -d

Access 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 logs for debugging.

Manual Installation (Without Docker)

For developers who prefer not to use Docker, you can set up the project manually:

  1. Clone the Repository:

    git clone https://github.com/vovanphu/nestjs-graphx
    cd nestjs-graphx
  2. Install Dependencies:

    yarn
  3. Environment Configuration:

    • Copy .env.development.local to .env and adjust the variables as needed for your local setup (e.g., database credentials).
  4. Database Setup:

    • Set up a MySQL database according to the configuration in .env.
    • Ensure the database is running and accessible.
  5. Run Services:

    • Start the cat-management service:
      yarn start:dev cat-management
    • Start the graphql-gateway service in another terminal:
      yarn start:dev graphql-gateway
  6. Access the GraphQL playground at http://localhost:4000.

Technologies

  • Framework: NestJS
  • Database: TypeORM (MySQL)
  • API Layer: GraphQL (Code-first)
  • Transport Layer: RabbitMQ (Microservices)
  • Containerization: Docker Compose

Complete Usage Guide

This comprehensive guide covers all aspects of using NestJS-GraphX, from basic entity definition to advanced relationship management and GraphQL API generation.

1. Entity Definition with Decorators

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.

Basic Entity Structure

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[];
}

Decorator Reference

@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
})

2. Microservice Implementation

Service Layer

import { TypeOrmCrudServiceFactory } from '@app/graphx';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService extends TypeOrmCrudServiceFactory(User) {}

Controller Layer

import { MicroserviceControllerFactory } from '@app/graphx';
import { Controller } from '@nestjs/common';

@Controller()
export class UserController extends MicroserviceControllerFactory(
  User,
  UserService,
) {}

Module Configuration

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UserService],
  controllers: [UserController],
  exports: [UserService],
})
export class UserModule {}

3. GraphQL Gateway Implementation

Proxy Service

import { CrudProxyServiceFactory } from '@app/graphx';
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService extends CrudProxyServiceFactory(
  User,
  'USER_SERVICE', // RabbitMQ client token
) {}

GraphQL Resolver

import { GraphqlCrudResolverFactory } from '@app/graphx';
import { Resolver } from '@nestjs/graphql';

@Resolver(User)
export class UserResolver extends GraphqlCrudResolverFactory(User, {
  providers: [UserService, UserProfileService, PostService],
}) {}

Gateway Module

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 {}

4. Generated GraphQL API

NestJS-GraphX automatically generates complete GraphQL APIs from your entity definitions.

Queries

# 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
      }
    }
  }
}

Mutations

# 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
    }
  }
}

5. Advanced Filtering

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
  }
}

Available Filter Operators

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

6. Relationship Management (AROP)

NestJS-GraphX enforces the Asymmetric Relation Ownership Pattern for clear relationship semantics.

Relationship Types

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[];

Ownership Rules

  • 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

7. Configuration

Environment Variables

# 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=development

TypeORM Configuration

TypeOrmModule.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],
})

8. Working Example

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

Design Philosophy

Asymmetric Relation Ownership Pattern (AROP)

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.

Core Concepts

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.

Goals

  • 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.

Examples

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.

Roadmap

  • 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.

Limitations

  • 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.

License

This project is licensed under the MIT License - see the LICENSE file for details.

About

Code-first GraphQL + Microservice toolkit for NestJS, with opinionated relationship modeling.

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages