Save Time and Stop Writing GraphQL Resolvers
- writing boilerplate code for basic CRUD functionalities
- implementing and maintaining GraphQL resolvers
- optimizing the resolvers for performance
- adding authentication and authorization
- implementing data validation and error checking mechanisms
- Boilerplate code: It requires a significant amount of boilerplate code for the CRUD operations
- Complexity: The resolvers become complex once you add filtering, aggregations, authorization, and custom business logic
- Performance: Inefficient implementation of resolvers can lead to an underperforming API
- Maintenance: Maintaining the resolvers and keeping them up-to-date with changes in the schema and data sources is time-consuming
Writing the GraphQL Resolvers manually
schema {
query: Query
}
type Query {
Album(id: Int!): Album
Albums: [Album!]!
Artist(id: Int!): Artist
Artists: [Artist!]!
Track(id: Int!): Track
Tracks: [Track!]!
}
type Album {
AlbumId: Int!
Artist: Artist
ArtistId: Int!
Title: String!
Tracks: [Track!]
}
type Artist {
ArtistId: Int!
Name: String
Albums: [Album!]
}
type Track {
Album: Album
AlbumId: Int
Bytes: Int
Composer: String
TrackId: Int!
MediaTypeId: Int!
Milliseconds: Int!
Name: String!
}
- a specific album
- all albums
- a specific artist
- all artists
- a specific track
- all tracks
import { readFileSync } from "node:fs";
import { createServer } from "node:http";
import { createSchema, createYoga } from "graphql-yoga";
import { GraphQLError } from "graphql";
import DataLoader from "dataloader";
import { useDataLoader } from "@envelop/dataloader";
import type { Album, Artist, Resolvers, Track } from "./generated/graphql";
import sql from "./db";
import { genericBatchFunction } from "./dataloader";
import { keyByArray } from "./utils";
const typeDefs = readFileSync("../schema.graphql", "utf8");
const resolvers: Resolvers = {
Query: {
Album: async (parent, args, context, info) => {
return (context.getAlbumsById as DataLoader<string, Album>).load(args.id.toString());
},
Albums: async (parent, args, context, info) => {
const albums = await (
context.getAllAlbums as DataLoader<string, Album[]>
).load('1');
for (const album of albums) {
(context.getAlbumsById as DataLoader<string, Album>).prime(
album.AlbumId.toString(),
album
);
}
const albumsByArtistId = keyByArray(albums, 'ArtistId');
for (const [ArtistId, albums] of Object.entries(albumsByArtistId)) {
(context.getAlbumsByArtistId as DataLoader<string, Album[]>).prime(
ArtistId,
albums
);
}
return albums;
},
Artist: async (parent, args, context, info) => {
return (context.getArtistsById as DataLoader<string, Artist>).load(
args.id.toString()
);
},
Artists: async (parent, args, context, info) => {
const artists = await (
context.getAllArtists as DataLoader<string, Artist[]>
).load('1');
if (!artists) {
throw new GraphQLError(`Albums not found.`);
}
for (const artist of artists) {
(context.getArtistsById as DataLoader<string, Artist>).prime(
artist.ArtistId.toString(),
artist
);
}
return artists;
},
Track: async (parent, args, context, info) => {
return (context.getTracksById as DataLoader<string, Track>).load(args.id.toString());
},
Tracks: async (parent, args, context, info) => {
const tracks = await (
context.getAllTracks as DataLoader<string, Track[]>
).load('1');
for (const track of tracks) {
(context.getTracksById as DataLoader<string, Track>).prime(
track.TrackId.toString(),
track
);
}
const tracksByAlbumId = keyByArray(tracks, 'AlbumId');
for (const [AlbumId, tracks] of Object.entries(tracksByAlbumId)) {
(context.getTracksByAlbumId as DataLoader<string, Track[]>).prime(
AlbumId,
tracks
);
}
return tracks;
},
},
Album: {
async Artist(parent, args, context, info) {
return (context.getArtistsById as DataLoader<string, Artist>).load(
parent.ArtistId.toString()
);
},
async Tracks(parent, args, context, info) {
const tracks = await (context.getTracksByAlbumId as DataLoader<string, Track[]>).load(
parent.AlbumId.toString()
);
for (const track of tracks) {
(context.getTracksById as DataLoader<string, Track>).prime(
track.TrackId.toString(),
track
);
}
return tracks
},
},
Artist: {
async Albums(parent, args, context, info) {
const albums = await (context.getAlbumsByArtistId as DataLoader<string, Album[]>).load(
parent.ArtistId.toString()
);
if (Array.isArray(albums)) {
for (const album of albums) {
(context.getAlbumsById as DataLoader<string, Album>).prime(
album.AlbumId.toString(),
album
);
}
}
return albums || [];
},
},
Track: {
async Album(parent, args, context, info) {
return (context.getAlbumsById as DataLoader<string, Album>).load(
parent.AlbumId!.toString()
);
},
}
};
export const schema = createSchema({
typeDefs,
resolvers,
});
const server = createServer(
createYoga({
schema,
plugins: [
useDataLoader(
"getAlbumsById",
(context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Album", id: "AlbumId" })
)
),
useDataLoader(
"getAllAlbums",
(context) =>
new DataLoader(async (keys: Readonly<string[]>) => {
const albums = await sql`SELECT * FROM ${sql("Album")}`;
return keys.map((key) => albums);
})
),
useDataLoader(
"getAlbumsByArtistId",
(context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Album", id: "ArtistId" }, true)
)
),
useDataLoader(
"getArtistsById",
(context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Artist", id: "ArtistId" })
)
),
useDataLoader(
"getAllArtists",
(context) =>
new DataLoader(async (keys: Readonly<string[]>) => {
const artists = await sql`SELECT * FROM ${sql("Artist")}`;
return keys.map((key) => artists);
})
),
useDataLoader(
"getTracksById",
(context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Track", id: "TrackId" })
)
),
useDataLoader(
"getTracksByAlbumId",
(context) =>
new DataLoader((keys: Readonly<string[]>) =>
genericBatchFunction(keys, { name: "Track", id: "AlbumId" }, true)
)
),
useDataLoader(
"getAllTracks",
(context) =>
new DataLoader(async (keys: Readonly<string[]>) => {
const tracks = await sql`SELECT * FROM ${sql("Track")}`;
return keys.map((key) => tracks);
})
),
],
})
);
server.listen(4000, () => {
console.info("Server is running on http://localhost:4000/graphql");
});
- the N+1 problem
- extending security rules per field in the schema
- adding gateway features (caching, rate limiting, etc.)
N+1 problem
Security Rules
Adding gateway features
Save time and resources
Building GraphQL APIs without writing resolvers
- using Hasura Cloud, which is the easiest way to use and build Hasura applications
- using Docker to self-host it
id
of type UUID (Primary Key)username
of type Textcountry
of type Text
How does that work
- a GraphQL type definition
- queries
- mutations
- subscriptions
query {
Album {
Title
}
}
SELECT
coalesce(json_agg("root"), '[]') AS "root"
FROM
(
SELECT
row_to_json(
(
SELECT
"_e"
FROM
(
SELECT
"_root.base"."Title" AS "Title"
) AS "_e"
)
) AS "root"
FROM
(
SELECT
*
FROM
"public"."Album"
WHERE
('true')
) AS "_root.base"
) AS "_root"
- Architecture of a high performance GraphQL to SQL engine
- Blazing fast GraphQL execution with query caching and Postgres prepared statements
Adding custom business logic
Remote Schemas
Hasura Actions
Event Triggers
Summary
Related reading