npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cbnsndwch/nest-zero-synced-queries

v0.9.0

Published

NestJS library for declarative Zero synced query handlers with decorator-based configuration

Downloads

1,500

Readme

@cbnsndwch/nest-zero-synced-queries

A lightweight NestJS library for defining Zero synced query handlers using decorators. Works seamlessly with regular NestJS controllers and providers, leveraging your existing authentication and guard infrastructure.

Features

  • Decorator-based: Use @SyncedQuery to define query handlers
  • Automatic discovery: Handlers are automatically discovered in controllers and providers at startup
  • Framework-agnostic auth: Use your own NestJS guards, interceptors, and parameter decorators
  • Controller-friendly: Works on regular @Controller classes alongside @Get, @Post, etc.
  • Parameter mapping: Use @QueryArg() decorator to explicitly map query arguments
  • Type-safe: Full TypeScript support with Zod schema validation

Installation

pnpm add @cbnsndwch/nest-zero-synced-queries

Related Packages

This library is part of the zero-sources monorepo, which provides utilities and integrations for @rocicorp/zero.

Zero Integration Packages

Example Applications

  • ZRocket - Full-featured chat application demonstrating Zero + NestJS patterns
  • MongoDB Source Server - Standalone MongoDB change source server

Quick Start

Get up and running in 3 simple steps:

1. Configure the Module

Import and configure SyncedQueriesModule in your app module:

import { Module } from '@nestjs/common';
import { SyncedQueriesModule } from '@cbnsndwch/nest-zero-synced-queries';

@Module({
  imports: [
    SyncedQueriesModule.forRoot({
      path: 'api/zero/get-queries'  // Optional: defaults to 'zero/get-queries'
    }),
    // Your feature modules...
  ]
})
export class AppModule {}

2. Define Your Schema

Create your Zero schema with query builder:

// schema.ts
import { createSchema } from '@rocicorp/zero';

export const schema = createSchema({
  tables: {
    todo: {
      columns: {
        id: 'string',
        title: 'string',
        completed: 'boolean',
        userId: 'string',
        createdAt: 'number'
      },
      primaryKey: 'id'
    }
  }
});

export const builder = schema.builder;

3. Add Synced Query Handlers

Decorate your methods with @SyncedQuery:

import { Controller, UseGuards } from '@nestjs/common';
import { SyncedQuery, QueryArg } from '@cbnsndwch/nest-zero-synced-queries';
import { AST } from '@rocicorp/zero';
import { z } from 'zod';
import { builder } from './schema.js';
import { JwtAuthGuard } from './auth/jwt-auth.guard.js';
import { CurrentUser } from './auth/current-user.decorator.js';

@Controller('todos')
@UseGuards(JwtAuthGuard)
export class TodosController {
  /**
   * Get all todos for the current user
   */
  @SyncedQuery('myTodos', z.tuple([]))
  async myTodos(@CurrentUser() user: { id: string }): Promise<AST> {
    return builder.todo
      .where('userId', '=', user.id)
      .orderBy('createdAt', 'desc').ast;
  }

  /**
   * Get a specific todo by ID (with permission check)
   */
  @SyncedQuery('todoById', z.tuple([z.string()]))
  async todoById(
    @CurrentUser() user: { id: string },
    @QueryArg(0) todoId: string
  ): Promise<AST> {
    return builder.todo
      .where('id', '=', todoId)
      .where('userId', '=', user.id).ast;
  }
}

That's it! The library automatically discovers your queries and exposes them via the configured HTTP endpoint.

Core Concepts

Authentication & Guards

Use your existing NestJS guards - the library passes the full HTTP request through:

@Controller('api')
@UseGuards(JwtAuthGuard, RolesGuard)  // Your guards work as normal
export class ApiController {
  @SyncedQuery('data', z.tuple([]))
  async getData(@CurrentUser() user: User) {
    return builder.data.where('userId', '=', user.id).ast;
  }
}

Query Arguments

Use @QueryArg(index) to access query parameters:

@SyncedQuery('postsByCategory', z.tuple([z.string(), z.number().optional()]))
async postsByCategory(
  @QueryArg(0) category: string,
  @QueryArg(1) limit = 10
): Promise<AST> {
  return builder.posts
    .where('category', '=', category)
    .limit(limit).ast;
}

Permission Filtering

Implement authorization by filtering results:

@SyncedQuery('sensitiveData', z.tuple([z.string()]))
async sensitiveData(
  @CurrentUser() user: User,
  @QueryArg(0) resourceId: string
): Promise<AST> {
  // Check permission
  const hasAccess = await this.checkPermission(user.id, resourceId);
  
  if (!hasAccess) {
    // Return query that matches nothing
    return builder.data.where('id', '=', '__NEVER_MATCHES__').ast;
  }
  
  return builder.data.where('id', '=', resourceId).ast;
}

Mixed Operations

Combine REST endpoints and synced queries in the same controller:

@Controller('posts')
@UseGuards(JwtAuthGuard)
export class PostsController {
  // REST endpoint for writes
  @Post()
  async createPost(@Body() dto: CreatePostDto) {
    return this.postsService.create(dto);
  }

  // Synced query for reads
  @SyncedQuery('allPosts', z.tuple([]))
  async allPosts(): Promise<AST> {
    return builder.posts.orderBy('createdAt', 'desc').ast;
  }
}

## Examples

### Simple Queries

No parameters needed - just use the authenticated user:

```typescript
@Controller('api')
@UseGuards(JwtAuthGuard)
export class ApiController {
  @SyncedQuery('myProfile', z.tuple([]))
  async myProfile(@CurrentUser() user: User): Promise<AST> {
    return builder.users.where('id', '=', user.id).ast;
  }

  @SyncedQuery('publicPosts', z.tuple([]))
  async publicPosts(): Promise<AST> {
    return builder.posts
      .where('isPublic', '=', true)
      .orderBy('createdAt', 'desc').ast;
  }
}

Queries With Parameters

@Controller('posts')
@UseGuards(JwtAuthGuard)
export class PostsController {
  @SyncedQuery('postById', z.tuple([z.string()]))
  async postById(@QueryArg(0) postId: string): Promise<AST> {
    return builder.posts.where('id', '=', postId).ast;
  }

  @SyncedQuery('postsByUser', z.tuple([z.string()]))
  async postsByUser(@QueryArg(0) userId: string): Promise<AST> {
    return builder.posts
      .where('userId', '=', userId)
      .where('isPublic', '=', true)
      .orderBy('createdAt', 'desc').ast;
  }
}

Optional Parameters

@SyncedQuery('searchPosts', z.tuple([z.string(), z.number().optional()]))
async searchPosts(
  @QueryArg(0) searchTerm: string,
  @QueryArg(1) limit = 20  // Default value for optional parameter
): Promise<AST> {
  return builder.posts
    .where('title', 'LIKE', `%${searchTerm}%`)
    .limit(limit).ast;
}

With Service Dependencies

Use constructor injection as normal:

@Controller('posts')
@UseGuards(JwtAuthGuard)
export class PostsController {
  constructor(
    private readonly permissionsService: PermissionsService
  ) {}

  @SyncedQuery('protectedPost', z.tuple([z.string()]))
  async protectedPost(
    @CurrentUser() user: User,
    @QueryArg(0) postId: string
  ): Promise<AST> {
    const canAccess = await this.permissionsService.canAccessPost(
      user.id,
      postId
    );

    if (!canAccess) {
      return builder.posts.where('id', '=', '__NEVER_MATCHES__').ast;
    }

    return builder.posts.where('id', '=', postId).ast;
  }
}

With Relations

Include related data using Zero's query builder:

@SyncedQuery('postWithComments', z.tuple([z.string()]))
async postWithComments(@QueryArg(0) postId: string): Promise<AST> {
  return builder.posts
    .where('id', '=', postId)
    .related('comments', q => 
      q.orderBy('createdAt', 'desc').limit(50)
    )
    .related('author').ast;
}

Real-World Example: ZRocket Chat App

Want to see how this library is used in a production application? Check out ZRocket, a chat application in this monorepo that demonstrates:

  • Mixed operations: REST endpoints for writes (send messages, create rooms) + synced queries for reads
  • Permission filtering: Room access checks, membership validation, public vs. private content
  • Complex queries: Search across accessible rooms, filter by room type, paginate results
  • Service integration: Using RoomAccessService for authorization in queries
  • Multiple controllers: MessagesController and RoomsController with different query patterns

Key files to explore:

Best Practices

  1. Co-locate operations: Put REST endpoints and synced queries in the same controller
  2. Use existing guards: Leverage your authentication infrastructure with @UseGuards()
  3. Filter, don't throw: Return queries that match nothing instead of throwing errors for unauthorized access
  4. Inject services: Use constructor injection to access business logic in queries
  5. Return AST: Always return Promise<AST> from query handlers (use .ast property)
  6. Type everything: Strongly type your user objects, query parameters, and return types
  7. Document queries: Add JSDoc comments describing what the query does and its parameters

API Reference

Decorators

  • @SyncedQuery(name, schema)

    • name: Unique query identifier string
    • schema: Zod schema for argument validation (e.g., z.tuple([z.string()]))
    • Use on controller or provider methods
    • Method must return Promise<AST>
  • @QueryArg(index)

    • index: Zero-based argument index
    • Injects the argument at that position from the query
    • Use alongside your own decorators (@CurrentUser(), etc.)

Module Configuration

  • SyncedQueriesModule.forRoot(options)
    • options.path: HTTP endpoint path (default: 'zero/get-queries')
    • Returns a DynamicModule for import

Services (Advanced)

  • SyncedQueryRegistry - Query handler registry (auto-injected)

    • getHandler(name) - Get handler by name
    • getQueryNames() - List all query names
    • hasQuery(name) - Check if query exists
    • getHandlerCount() - Total handler count
  • SyncedQueryTransformService - Query execution service (auto-injected)

    • Used internally by the controller
    • Handles query execution and AST conversion

Troubleshooting

Query not found

  • Ensure your controller/provider is registered in a module
  • Check that SyncedQueriesModule.forRoot() is imported
  • Verify the query name matches exactly

Authentication not working

  • Ensure your guard is applied: @UseGuards(YourAuthGuard)
  • Check that request.user is populated by your auth strategy
  • Guards receive the full HTTP request object

TypeScript errors

  • Install peer dependencies: @nestjs/common, @nestjs/core, reflect-metadata, rxjs, zod
  • Ensure emitDecoratorMetadata is enabled in tsconfig.json

Contributing

Found a bug or have a feature request? Please open an issue on GitHub.

Contributions are welcome! See CONTRIBUTING.md for guidelines.

License

MIT • Part of the zero-sources monorepo