Guidelines to be followed when developing on the GraphQL gateway. They are categorised as follows:
- β DO
- π CONSIDER
- π AVOID
- π« DO NOT
Other resources:
Index
A unit of data you are asking for in a Schema, which ends up as a field in your JSON response data.
GraphQL types are nullable by default but you can provide a non-null variant with NonNull<T>
.
public NonNull<string> Title => Dto.Title;
GraphQL Conventions provides a Description
attribute that can be used on input or output fields.
public async Task<Connection<Member>> Members(
[Inject] ITeamService teamService,
[Inject] IMemberService memberService,
int? first, int? last, string after, string before,
[Description("Group id to filter members")] string groupId,
[Description("Only Disable members")] bool? disabledOnly,
[Description("Roles to include")] List<string> roles)
[Description("The gender of the team.")]
public Gender Gender => Dto.Gender;
A special field encoded to base64 that allows to have unique Ids on a Graph.
β
DO - Use Id.New<T>
from GraphQL Conventions for Id fields
This will create a base64 representation of Type + Id instead of the Id.
[Description("The graphql Id of the team.")]
public Id Id => Id.New<Team>(Dto.TeamId);
GraphQL Conventions provides an easy way to decode GraphQL Ids to be used on internal service calls
var internalGroupId = groupId.IdentifierForType<Group>();
Where not possible, create a migration path for clients by supporting both and deprecating the InternalId
field.
[Description("The hudl Id of the team.")]
[Obsolete("Use Id instead")]
public string InternalId => Dto.TeamId;
If you need to expose the raw Id (ex: REST call) use InternalId
property instead of decoding GraphQL Ids on the client.
Describes an output value for a field.
This will allow you to have well-defined boundaries between your GraphQL layer and your external data providers.
public Organization Organization => Organization.FromDto(Dto.School);
Union types are very similar to interfaces, but they don't get to specify any common fields between the types.
[Description("Represents a searchable resource in the video and library domains.")]
public class LibraryContent : Union<VideoItem, PlaylistItem, FileItem>, INode
{
public Id Id =>
Id.New<LibraryContent>(DateTime.UtcNow.ToString(CultureInfo.InvariantCulture));
}
Functions that determine how a field in your GraphQL schema is computed on the backend.
Prefer direct mapping from a Dto if you can use one otherwise use a method resolver.
public bool IsSystemGroup => Dto.IsSystemGroup;
π CONSIDER - Using a method resolver when the field is not part of the Dto or you need to pass in arguments
There a few examples where the Dto does not have all the information that we need so we use a function resolver to fetch the information.
[Description("Group Video Activity")]
public async Task<VideoActivityTypes.VideoActivity> VideoActivity(
IRootContext context,
[Inject] IMemberService memberService,
[Inject] IVideoActivityService videoActivityService)
{
var memberIds = await memberService.GetMemberIdsForGroup(Dto.GroupId);
var videoActivityDto = await videoActivityService.GetVideoActivityForMembers(memberIds);
return VideoActivityTypes.VideoActivity.FromDto(videoActivityDto);
}
A single query, mutation, or subscription that can be interpreted the GraphQL execution engine.
Group your operations per type and domain. This will help navigate through the schema
Ex: TeamQuery
, TeamMutation
, TeamSubscription
public GraphQLSchema(IDependencyInjector dependencyInjector = null)
{
_requestHandler = RequestHandler
.New()
.WithDependencyInjector(dependencyInjector)
.WithQuery<TeamQuery>()
.WithMutation<TeamMutation>()
.WithoutValidation()
.Generate();
}
A set of key-value pairs attached to a specific field. Arguments can be literal values or variables.
This will be picked up by the engine and throw errors before it hits the resolvers.
[Description("Get invite code by id")]
public async Task<InviteCode> InviteCode(
[Description("Invite code id")] NonNull<string> inviteCodeId)
A read-only fetch operation to request data from a GraphQL service.
Naming things can be hard but your query names should be the same as the Type its quering. There are a few exceptions
[Description("Get team by id or customer account id")]
public async Task<Team> Team(
IRootContext context,
[Description("Team id")] string teamId,
[Description("Customer account id")] string customerAccountId)
[Description("Get a list of my teams")]
public async Task<List<Team>> MyTeams(
IRootContext context,
[Description("Show teams that have one of these membership roles")] List<string> membershipRoles,
[Description("Show teams that have all of these features")] List<string> requiredFeatures)
An operation for creating, modifying and destroying data.
Allow clients to query on the mutation to save roundtrips to the server.
public async Task<Group> AddGroup(
IRootContext context,
[Description("Group")] NonNull<GroupInput> group,
[Description("Team id")] NonNull<string> teamId)
GraphQL supports mutations without a return value but that often leads to subsequent requests from the client. It's important to inform clients of the result of the mutation and allow them to ignore it if they choose to do so.
A real-time GraphQL operation. A Subscription is defined in a schema like queries and mutations.
TBD
TBD
Authorization is a type of business logic that describes whether a given user/session/context has permission to perform an action or see a piece of data.
β DO - Use AuthorizationContextDto
AuthorizationContextDto holds user info on the call context. This can also be passed as a param to other services
The Schema authorization happens on the controller level. Authorizing access to data should be done on the dataloader
or delegated to the service passing the AuthorizationContextDto
as a param
Most client apis follow the relay conventions
Connections allows the client to paginate the results of coming from the api.
public async Task<Connection<Member>> Members(//other inputs
int? first, int? last, string after, string before)
{
//...
return memberDtos
.Select(Types.Members.Output.Member.FromDto)
.ToConnection(first, after, last, before, memberDtos?.Count);
}
π CONSIDER - Using PageInfo
from GraphQL Conventions when working with Connections
GraphQL Conventions provides a very handy PageInfo
that can be used for paginating results on the client.
[Description("Information about pagination in a connection.")]
public class PageInfo
{
[Description("When paginating forwards, are there more items?")]
public bool HasNextPage { get; set; }
[Description("When paginating backwards, are there more items?")]
public bool HasPreviousPage { get; set; }
[Description("When paginating backwards, the cursor to continue.")]
public Cursor StartCursor { get; set; }
[Description("When paginating forwards, the cursor to continue.")]
public Cursor EndCursor { get; set; }
}
Data loaders are classes that encapsulate fetching data from a particular service, with built-in support for caching, deduplication, and error handling.
GraphQL is designed in a way that allows you to write clean code on the server, where every field on every type has a focused single-purpose function for resolving that value. However without additional consideration, a naive GraphQL service could be very "chatty" or repeatedly load data from your databases.
TBD
TBD - Folder and project structure
TBD - Talk about boundary restrictions
This attribute tells GraphQL Conventions to use its dependency injector to resolve service dependencies
public async Task<Connection<Member>> MembersToRemove(
[Inject] IMemberService memberService,
int? first, int? last, string after, string before)