
createDataBridge()
import { createDataBridge } from '@green-stack/schemas/createDataBridge'
- createDataBridge.ts
To create a DataBridge
, simply provide call createDataBridge()
with with the input and output schemas and some additional metadata:
const healthCheckBridge = createDataBridge({
// Input and Output
inputSchema: HealthCheckInput,
outputSchema: HealthCheckOutput,
// Basic Metadata
resolverName: 'healthCheck',
resolverType: 'query', // 'query' | 'mutation'
// REST Metadata
apiPath: '/api/health',
allowedMethods: ['GET'], // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'GRAPHQL'
// Optional Metadata
resolverArgsName?: 'healthCheckArgs', // Custom Args name for GraphQL schema and queries
})
Why “Data Bridges”?
Schemas serve as the single source of truth for your data shape. But what about the shape of your APIs?
By combining input and output schemas into a bridge
file, and adding some API metadata, bridges serve as the source of truth for your API resolver:
Reusable Client-Server Contract
Think of a “Databridge” as a literal bridge between the front and back-end.
It’s a metadata object you can use from either side to provide / transform / extract:
- ✅ Route handler args from request params / body
- ✅ Input and output types + validation + defaults
- ✅ GraphQL schema definitions for
schema.graphql
- ✅ The query string to call our GraphQL API with
Recommended file conventions
- updatePost.bridge.ts
There’s two reasons we suggest you define this “DataBridge” in a separate file:
-
- Reusability: If kept separate from business logic, you can reuse it in both front and back-end code.
-
- Consistency: Predicatable patterns make it easier to build automations and generators around them.
For this reason, we suggest you add
.bridge.ts
to your bridge filenames.
Using a DataBridge
You can use the resulting DataBridge
in various ways, depending on your needs:
Flexible Resolvers
To use the data bridge we just created to bundle together the input and output types with our business logic, create a new resolver file and passing the bridge as the final arg to createResolver()
at the end.
The first argument is your resolver function will contain a function with your business logic:
import { createResolver } from '@green-stack/schemas/createResolver'
import { updatePostBridge } from './updatePost.bridge'
import { Posts } from '@db/models'
/** --- updatePost() ---- */
/** -i- Update a specific post. */
export const updatePost = createResolver(async ({
args, // <- Auto inferred types (from 'inputSchema')
context, // <- Request context (from middleware)
parseArgs, // <- Input validator (from 'inputSchema')
withDefaults, // <- Response helper (from 'outputSchema')
}) => {
// Validate input and apply defaults, infers input types as well
const { slug, ...postUpdates } = parseArgs(args)
// -- Context / Auth Guards / Security --
// e.g. use the request 'context' to log out current user
const { user } = context // example, requires auth middleware
// -- Business Logic --
// e.g. update the post in the database
const updatedPost = await Posts.updateOne({ slug }, postUpdates)
// -- Respond --
// Typecheck response and apply defaults from bridge's outputSchema
return withDefaults({
slug,
title,
content,
})
}, updatePostBridge)
You pass the bridge (☝️) as the second argument to
createResolver()
to:
- 1️⃣ infer the input / arg types from the bridge’s
inputSchema
- 2️⃣ enable
parseArgs()
andwithDefaults()
helpers for validation, hints + defaults
The resulting function can be used as just another async function anywhere in your back-end.
The difference with a regular function, since the logic is bundled together with its data-bridge / input + output metadata, is that we can easily transform it into APIs:
API Route Handlers
- route.ts ← ✅ Add route handler here
You can create a new API route by exporting a GET
/ POST
/ UPDATE
/ DELETE
handler assigned to a createNextRouteHandler()
that wraps your “bridged resolver”:
import { updatePost } from '@app/resolvers/updatePost.resolver'
import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
/* --- Routes ------------ */
export const UPDATE = createNextRouteHandler(updatePost)
// Automatically extracts (☝️) args from url / search params
// based on the zod 'inputSchema'
// If you want to support e.g. POST (👇), same deal (checks request body too)
export const POST = createNextRouteHandler(updatePost)
What createNextRouteHandler()
does under the hood:
-
- extract the input from the request context
-
- validate it
-
- call the resolver function with the args (and e.g. token / session / request context)
-
- return the output from your resolver with defaults applied
Check Next.js Route Handlers to understand supported exports (like
GET
orPOST
) and their options.
Restart your dev server or run
npm run link:routes
to make sure your new API route is available.
GraphQL Resolvers
We made it quite easy to enable GraphQL for your resolvers. The flow is quite similar.
In the same file, add the following:
import { updatePost } from '@app/resolvers/updatePost.resolver'
import { createNextRouteHandler } from '@green-stack/schemas/createNextRouteHandler'
import { createGraphResolver } from '@green-stack/schemas/createGraphResolver'
/* --- Routes ------------ */
// exports of `GET` / `POST` / `PUT` / ...
/* --- GraphQL ----------- */
export const graphResolver = createGraphResolver(updatePost)
// Automatically extracts input (☝️) from graphql request context
After exporting graphResolver
here, restart the dev server or run npm run build:schema
manually.
This will:
-
- pick up the
graphResolver
export
- pick up the
-
- put it in our list of graphql compatible resolvers at
resolvers.generated.ts
in@app/registries
- put it in our list of graphql compatible resolvers at
-
- recreate
schema.graphql
from input & output schemas from registered resolvers
- recreate
You can now check out your GraphQL API playground at /api/graphql
Universal Data Fetching
The easiest way to create a fetcher is to use the bridgedFetcher()
helper:
import { updatePostBridge } from './updatePost.bridge'
// ☝️ Reuse your data bridge
import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
// ☝️ Universal graphql fetcher that can be used in any JS environment
/* --- updatePostFetcher() --------- */
export const updatePostFetcher = bridgedFetcher(updatePostBridge)
This will automatically build the query string with all relevant fields from the bridge.
To write a custom query with only certain fields, you can use our graphql()
helper with bridgedFetcher()
:
import { ResultOf, VariablesOf } from 'gql.tada'
// ☝️ Type helpers that interface with the GraphQL schema
import { graphql } from '../graphql/graphql'
// ☝️ Custom gql.tada query builder that integrates with our types
import { bridgedFetcher } from '@green-stack/schemas/bridgedFetcher'
// ☝️ Universal graphql fetcher that can be used in any JS environment
/* --- Query ----------------------- */
// VSCode and gql.tada will help suggest or autocomplete thanks to our schema definitions
export const updatePostQuery = graphql(`
query updatePost ($updatePostArgs: UpdatePostInput) {
updatePost(args: $updatePostArgs) {
slug
title
body
}
}
`)
// ⬇⬇⬇ automatically typed as ⬇⬇⬇
// TadaDocumentNode<{
// updatePost(args: Partial<Post>): {
// slug: string | null;
// title: boolean | null;
// body: boolean | null;
// };
// }>
// ⬇⬇⬇ can be turned into reusable types ⬇⬇⬇
/* --- Types ----------------------- */
export type UpdatePostQueryInput = VariablesOf<typeof updatePostQuery>
export type UpdatePostQueryOutput = ResultOf<typeof updatePostQuery>
/* --- updatePostFetcher() --------- */
export const updatePostFetcher = bridgedFetcher({
...updatePostBridge, // <- Reuse your data bridge ...
graphqlQuery: updatePostQuery, // <- ... BUT, use our custom query
})
Whether you use a custom query or not, you now have a fetcher that:
- ✅ Uses the executable graphql schema serverside
- ✅ Can be used in the browser or mobile using fetch
Want to know even more? Check the Universal Data Fetching Docs.
Resolver Form State
- useUpdatePostFormState.tsx ←
To further keep your API’s input and form state in sync, you can link the input schema to a form state hook using useFormState()
:
import { useFormState } from '@green-stack/forms/useFormState'
// Create a set of form state utils to use in your components
const formState = useFormState(updatePostBridge.inputSchema, {
initialValues: { ... },
// ... other options
})
More about this in the Form Management Docs.