🚀Announcing Flightcontrol - Easily Deploy Blitz.js and Next.js to AWS 🚀
Back to Documentation Menu

Server Utilities

Topics

Jump to a Topic

Blitz exports a resolver object which contains a few utilities. "Resolver" as used here and for queries and mutations refers to a function that takes some input and "resolves" that into some output or side effect. This term is commonly used for GraphQL. A resolver is separate from an API endpoint, because in an API endpoint you have full access to the Node request and response objects and you are responsible for managing the http stuff. But resolvers are a layer removed from raw API handlers.

The below utilities are exported under the resolver object instead of resolverPipe and resolverZod since they are commonly used together. This cleans up your imports and makes it simple to discover any other utilities on the resolver object.

resolver.pipe

This is a functional pipe that makes it easier and cleaner to write complex resolvers.

A pipe automatically pipes the output of one function into the next function. Here's a non-Blitz example:

// Without pipe
export function (input1) {
  const output1 = function1(input1)
  const output2 = function2(output1)
  const output3 = function3(output2)
  return output3
}

// With pipe
export pipe(
  function1,
  function2,
  function3,
}

Benefits:

  • It reduces the need for explicit TypeScript types
  • One line to validate input data with a zod schema with resolver.zod()
  • One line to authorize users with resolver.authorize()
  • Enables functional composition

Example

import { resolver } from "@blitzjs/rpc"
import db from "db"
import * as z from "zod"

export const CreateProject = z.object({
  name: z.string(),
  dueDate: z.date().optional(),
  orgId: z.number().optional(),
})

export default resolver.pipe(
  resolver.zod(CreateProject),
  resolver.authorize(),
  // Set default orgId
  (input, { session }) => ({
    ...input,
    orgId: input.orgId ?? session.orgId,
  }),
  async (input, ctx) => {
    console.log("Creating project...", input.orgId)
    const project = await db.project.create({
      data: input,
    })
    console.log("Created project")
    return project
  }
)
Info

The input type of the entire composed resolver function is determined by the input type of the first argument to pipe.

This means you will almost always want resolver.zod() to be the first in the pipe.

API

Arguments

It accepts from 1 to N functions which take input data as first argument and middleware ctx as the second argument.

The output of the first function is the first input argument of the next function. The output of the last function is the final resolver result data that will be sent to the client.

The TypeScript types are automatically inferred from one function to the next.

// 1 function
const resolver = resolver.pipe((input: ResolverInput, ctx: Ctx) => ResolverOutput)
// 2 functions
const resolver = resolver.pipe(
  (input: ResolverInput, ctx: Ctx) => A),
  (input: A, ctx: Ctx) => ResolverOutput),
)
// 3 functions
const resolver = resolver.pipe(
  (input: ResolverInput, ctx: Ctx) => A),
  (input: A, ctx: Ctx) => B),
  (input: B, ctx: Ctx) => ResolverOutput),
)
// ... etc

Returns

This returns a composed function of type (input, ctx) => Promise<result> which is the standard resolver type.

resolver.zod

This is a handly utility for using Zod, an awesome input validation library. It takes a zod schema and runs schema.parseAsync on the input data.

Note: If you want to run the synchronous version; schema.parse, you can pass in the value "sync" to the second parameter of resolver.zod E.g.: resolver.zod(CreateProject,"sync")

You don't have to use this. You can add const safeInput = CreateProject.parse(input) directly in your main resolver function. But then you also have to manually type the function interface. The utility does everything in one step, setting input type and validating the input data at runtime.

Example

import { resolver } from "@blitzjs/rpc"
import * as z from "zod"

export const CreateProject = z.object({
  name: z.string(),
  dueDate: z.date().optional(),
  orgId: z.number().optional(),
})

export default resolver.pipe(
resolver.zod(CreateProject),
async (input, ctx) => { // stuff } )

API

const pipeFn = resolver.zod(MyZodSchema)

Arguments

  • ZodSchema: a zod schema
    • Required

Returns

A function to give resolver.pipe of type (rawInput, ctx: Ctx) => validInput

resolver.authorize

Using resolver.authorize in resolver.pipe is a simple way to check whether the user has the authorization to call the query or mutation or not. For this, blitz uses session.$authorize from the context object.

Example

import { resolver } from "@blitzjs/rpc"

export default resolver.pipe(
resolver.authorize(), // resolver.authorize('admin'),
async (input, ctx) => { // stuff } )

API

const pipeFn = resolver.authorize(...isAuthorizedArgs)

Arguments

Returns

A function to give resolver.pipe of type (input, ctx: Ctx) => input

paginate

This is a handy utility for query pagination

Example

import { paginate, Ctx } from "@blitzjs/rpc"
import db, { Prisma } from "db"

interface GetProjectSInput
  extends Pick<
    Prisma.ProjectFindManyArgs,
    "where" | "orderBy" | "skip" | "take"
  > {}

export default async function getProjects(
  { where, orderBy, skip = 0, take = 100 }: GetProjectsInput,
  ctx: Ctx
) {
  ctx.session.$authorize()

  const {
    items: projects,
    hasMore,
    nextPage,
    count,
  } = await paginate({
    count: () => db.project.count({ where }),
    query: (paginateArgs) =>
      db.project.findMany({ ...paginateArgs, where, orderBy }),
    skip,
    take,
  })

  return {
    projects,
    nextPage,
    hasMore,
    count,
  }
}

API

const paginationData = await paginate(paginateArguments)

Arguments

  • count: () => Promise <number>
    • Required
    • A function that returns a promise which resolves to a number
  • query: (payload: { skip: number; take: number }) => Promise<T>
    • Required
    • A function that receives pagination payload and returns anything (type of return will be inherited by the pagination result).
  • skip: number
    • How many rows to skip
  • take: number
    • How many rows to take
  • maxTake: number
    • The maximum value allowed for take. It protects your DB when "take" is sent by the user.
    • Defaults to 250.

Returns

  • paginationData
    • items: T (Data resolver by the query function)
    • nextPage: { skip: number, take: number } | null (Next page pagination payload)
    • hasMore: boolean (True if there are more items to load.)
    • count: number (Total number of items. Resolved by the count function)

invokeWithCtx

This is for imperatively invoking queries/mutations on the server.

Example

import { Link } from "next"
import { PromiseReturnType } from "blitz"
import { invokeWithCtx } from "@blitzjs/rpc"
import {
  BlitzPage,
  Routes
} from "@blitzjs/next"
import { gSSP } from "app/blitz-server"
import getProducts from "app/products/queries/getProducts"

type PageProps = {
  products: PromiseReturnType<typeof getProducts>
}

export const getServerSideProps = gSSP(async ({
  ctx
}) => {
  const products = await invokeWithCtx(
    getProducts,
    { orderBy: { id: "desc" } },
    ctx
  )

  return {
    props: { products },
  }
}

const Page: BlitzPage<PageProps> = function ({ products }) {
  return (
    <div>
      <h1>Products</h1>
      <div id="products">
        {products.map((product) => (
          <p key={product.id}>
            <Link href={Routes.Product({ handle: product.handle })}>
              <a>{product.name}</a>
            </Link>
          </p>
        ))}
      </div>
    </div>
  )
}
export default Page

API

const results = await invokeWithCtx(resolver, resolverInputArguments, ctx)

Arguments

  • resolver: A Blitz query resolver or mutation resolver
    • Required
  • resolverInputArguments
    • Required
    • The arguments that will be passed to resolver
  • Other
    • ctx — Blitz session context

Returns

  • results
    • The exact results returned from the resolver

Idea for improving this page? Edit it on GitHub.