You can also find all 100 answers here 👉 Devinterview.io - GraphQL
GraphQL is a highly efficient data query and manipulation language paired with a server runtime. It's designed to optimize data fetching for clients by allowing them to specify the shape and structure of the data they need.
In contrast, REST is an architectural style where clients interact with server endpoints (resources) using a standard set of HTTP methods.
-
Efficiency: GraphQL minimizes data over-fetching and under-fetching, ensuring that clients receive only the necessary data. In contrast, REST endpoints have fixed responses, potentially leading to data redundancy or under-supply.
-
Flexibility: With GraphQL, clients can specify the exact shape of the data they need. This is beneficial for applications with varying data requirements. REST endpoints deliver a pre-defined data set in their responses.
-
Versioning & Documentation: GraphQL obviates the need for versioning and keeps documentation centralized. In REST, version management and documentation are typically separate from the API, complicating the process.
-
Data Validation: GraphQL servers validate and type-check incoming requests. In REST, this is typically the client's responsibility.
-
Network Efficiency: GraphQL often makes fewer network calls when acquiring related resources, compared to REST, which could necessitate multiple requests for the same resources.
-
Tooling & Ecosystem: Both approaches enjoy extensive tooling support, but GraphQL's real-time introspection, strong type system, and auto-generation of documentation sets it apart. Additionally, GraphQL clients can benefit from query caching strategies.
-
Cache Control: REST APIs provide various caching strategies using standard HTTP mechanisms. Apollo Federation in GraphQL further refines these strategies, offering finer control over caching across federated services.
-
Performance Optimizations: Both REST and GraphQL can employ mechanisms like request throttling, but GraphQL's ability to batch multiple data requests into a single query contributes to improved performance.
-
Response Compression: For reducing data size, GraphQL can utilize techniques like query result compression, while REST APIs can use response compression.
-
Data Consistency: In a reliable setup, both REST and GraphQL can provide data consistency. However, when using GraphQL with subscriptions, real-time updates can be more straightforward to implement.
-
GraphQL: Ideal for applications with diverse or changing data requirements, dynamic UIs demanding real-time data, or microservice architectures. Also suitable when the client and server are developed and maintained by the same team, ensuring end-to-end compatibility.
-
REST: Recommended when the application has straightforward data requirements, relies on well-established data models, or the client's front-end architecture implements one-to-one mapping with the server's resources.
Here is the GraphQL code:
type Query {
# Retrieves a list of all users
allUsers: [User]
}
type User {
id: ID!
name: String!
email: String!
}
# Example Query: Fetch all user names and emails
query {
allUsers {
name
email
}
}
Now, the equivalent REST endpoint for reference:
GET /api/users
Response Body:
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@gmail.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane.smith@gmail.com"
}
]
The GraphQL architecture revolves around several key components, each serving a distinct role in the broader ecosystem.
-
Client: The client initiates data requests, specifying their shape and content through GraphQL queries. This permits more streamlined data access compared to traditional REST operations.
-
Server: It's the server's task to process incoming GraphQL queries and deliver the relevant data in response. This paradigm of executing GraphQL queries is defined as "query execution."
-
Schema: The schema serves as the contract between the client and the server, establishing the type system, namely the types of data available for interaction (e.g.,
User
,Post
) and the operations that can be performed (e.g.,Query
,Mutation
). -
Resolver Functions: These functions are associated with each field in a GraphQL type and dictate how that field's data should be retrieved or manipulated. For instance, a resolver for a
user
field within aQuery
type might describe how to fetch a user. -
Query Validator and Executor: After a query is received, the server performs a two-stage process: validation (ensuring the query adheres to the schema) and execution (fetching data as per the validated query and its resolvers).
-
Data Source: Represents the origin of real data, like a database. Each resolver can pull data from different data sources, which can improve modularity and scalability.
-
Operations: On the server, these can be of two types:
Query
for reading data andMutation
for modifying data. Additionally, one can writeSubscription
operations for real-time data updates.
-
Query: Sent by the client to specify the data requirements. It's validated, and only if successful, the server operates on it.
-
Validation: The query is verified against the schema to confirm its correctness. If any elements in the query violate the schema, an error is sent back to the client.
-
Execution: The server employs the resolved functions to retrieve or process the required data, responding with the results in the expected format.
A GraphQL query specifies the data you seek and the shape you expect it back in. It's composed of fields nested within each other, forming a query tree.
-
Fields: The basic unit of a query, representing a piece of data you want to retrieve.
-
Arguments: Used to customize the result of a field with specific parameters.
-
Aliases: Enables multiple references to the same field with different configurations.
-
Fragments: Reusable group of fields, improving the organization of query documents.
-
Directives: Provide conditional query execution and value manipulation.
-
Operation Types: Defines whether the query is for fetching data (
query
) or mutating data (mutation
).
GraphiQL Example:
query {
currentUser {
name
posts(last: 5)
}
}
JSON Response that May be Returned:
{
"user": {
"name": "John Doe",
"posts": [
// Array of post objects
]
}
}
Here is the Node.js code:
var query = /* GraphQL */ `{
currentUser {
name
posts(last: 5)
}
}`;
firebaseUserModel.query(query).then(response => {
console.log("User Data:", response);
});
Let's look into the core features of GraphQL, a query language and runtime designed by Facebook to enable efficient data communication between the client and server.
-
Declarative Data Fetching: Empowers clients to request exactly what they need. With REST, developers often over-fetch or under-fetch data, causing inefficiencies in both data transmission and rendering. In contrast, GraphQL allows clients to specify their data requirements, leading to more efficient and specific data retrieval.
- Example: Here is a structural query defined with GraphQL, automatically fetching necessary data.
query { user(id: "123") { name posts { title content } } }
-
Hierarchical Structure: Data is structured as a graph. GraphQL directives, which are used to annotate and modify existing schema types, can be coupled with the query language to impose rules on the query execution. For instance,
@include
and@skip
enable conditional fetching of fields.- Example: The following query demonstrates conditional fetching using directives.
query { user(id: "123") { name posts { title content comments @include(if: $showComments) } } }
-
Strongly-Typed Schema: Businesses and developers can benefit from type safety, reducing errors and unexpected behaviors. Every GraphQL API is formed from a set of well-defined object types with precise fields. This type system is backed by a schema that needs to be explicitly declared and shared.
-
Example: The schema-first approach uses SDL (Schema Definition Language) to define types and their relationships.
type User { id: ID! name: String! age: Int }
-
-
Single Endpoint for All Operations: Simplifies the management and orchestration of data, as a single endpoint is utilized for all data interactions. This differs from REST-centric approaches, where endpoints are typically specific to resources or actions, leading to potential confusion in larger systems.
-
Built-in Documentation: Provides auto-generated and readily-accessible documentation, easing the burden of maintaining external documentation. This "graphiql" in-browser environment, often utilized during the development stage, allows an interactive exploration of the available API and assists in writing precise queries.
-
Introspection: Enables tools to query the GraphQL server itself for its schema. This meta-level functionality enhances the extensibility and productivity of developers by facilitating functionalities like dynamic query generation and language or platform-specific tooling.
-
Real-time Data: Through Subscriptions, GraphQL offers a method for the server to actively push data to clients, creating remarkable opportunities for real-time interactions in modern apps. Whether it's chat applications or collaborative documents, this feature provides a seamless experience for end-users.
- Example: A GraphQL query incorporating real-time data acquisition.
subscription { newUser { id name } }
-
Reduction in Over/Under Fetching: Empowers clients to receive precisely the data they need, eradicating the efficiency challenges associated with over-fetching and under-fetching.
-
Improved Frontend Autonomy: Fosters frontend independence, allowing teams to evolve their applications without necessitating corresponding backend alterations.
-
Enhanced Tooling and Efficiency: The robust type system and schema enable advanced tooling and provide developers with comprehensive insights even at the time of query construction.
-
Adaptability and Evolvability: Facilitates a more streamlined development lifecycle, particularly in contexts featuring rapid evolution or requirements for distinct client applications like web, mobile, and IoT.
GraphQL Schema serves as the contract between client and server, outlining available data types, their relationships, and the API surface.
Data in GraphQL is wrapped in object types. Each type comprises a set of fields, which can be other object types or scalar types.
Example:
The User
type may comprise fields like id
(a scalar) and posts
(a list of the Post
type).
Scalar types are the atomic data types of GraphQL. They include standard types like String
, Int
, Float
, Boolean
, and ID
. Custom scalar types allow for tailored behaviors, like DateTime
for consistent date representation.
Both are entry points for the client:
- Query: Specifies available read operations clients can invoke.
- Mutation: Defines write operations.
Example:
type Mutation {
createUser(name: String!): User
}
These represent data the client sends to the server during mutations. They are similar to object types but exclusively used as input.
Example:
input UserInput {
name: String!
}
These facilitate complex type hierarchies.
- Interfaces: A collection of shared fields which GraphQL types can implement.
- Unions: Acts as an "or" statement, allowing multiple types to be returned.
Example:
interface Post {
content: String!
author: User!
}
type TextPost implements Post {
content: String!
author: User!
wordCount: Int!
}
type ImagePost implements Post {
content: String!
author: User!
imageUrl: String!
}
type Query {
allPosts: [Post]!
}
These are like annotations and can modify the behavior of an operation. While they don't define data types, they are still part of the type system.
Example:
directive @auth on FIELD_DEFINITION
type User {
id: ID
email: String! @auth
password: String! @auth
}
These are custom object types that enumerate a defined set of options.
Example:
enum UserRole {
ADMIN
USER
}
A single GraphQL schema can consist of various types, including those mentioned above. Every field in the schema needs to resolve to one of the defined types.
- Clear Communication: Schemas act as a clear API contract between the client and the server.
- Structured Data: They define data types and relationships, ensuring a structured data model.
- Safety: The schema helps in error detection and makes sure data types are consistently handled across the application.
- Ease of Collaboration: Schemas ensure that a front-end and back-end developer can work independently as long as they adhere to the schema contract.
- Documentation Generation: Schemas can be used to auto-generate developer-friendly documentation, promoting clarity and comprehension.
A GraphQL schema, like any software, is subject to change and evolution. However, changes need to be managed to ensure query compatibility. Visual tools, versioning strategies, and strong communication between teams are vital for seamless schema evolution.
Below is the Node.js code:
-
Define Schema and Types with GraphQL
const { GraphQLObjectType, GraphQLSchema, GraphQLString, GraphQLList, GraphQLNonNull, GraphQLInterfaceType } = require('graphql'); const UserType = new GraphQLObjectType({ name: 'User', fields: () => ({ id: { type: GraphQLString }, name: { type: new GraphQLNonNull(GraphQLString) }, posts: { type: new GraphQLList(PostType), resolve: (user) => getUserPosts(user.id), }, }), }); const PostType = new GraphQLInterfaceType({ name: 'Post', fields: () => ({ content: { type: GraphQLString }, author: { type: UserType }, }), resolveType: (post) => { if (typeof post.wordCount !== 'undefined') { return 'TextPost'; } return 'ImagePost'; }, }); module.exports = new GraphQLSchema({ types: [UserType, PostType], query: new GraphQLObjectType({ name: 'RootQueryType', fields: { allPosts: { type: new GraphQLList(PostType), resolve: getAllPosts, }, }, }), });
-
Specify Directives
const { GraphQLDirective, DirectiveLocation, GraphQLString } = require('graphql'); const authDirective = new GraphQLDirective({ name: 'auth', description: 'Directive to control access to secure fields.', locations: [DirectiveLocation.FIELD_DEFINITION], args: { requires: { type: new GraphQLNonNull(GraphQLString) }, }, resolve: (next, src, args, context) => { // Implement your authentication logic return next(); }, }); module.exports = new GraphQLSchema({ directives: [authDirective], types: [UserType], query: new GraphQLObjectType({ name: 'RootQueryType', fields: { user: { type: UserType, resolve: (root, args, context) => { // Provide field values here }, users: { type: new GraphQLList(UserType), auth: { requires: 'admin', }, resolve: (root, args, context) => { // Provide field values here }, }, }, }, }), });
Fields form the basis of GraphQL data retrieval. Whereas REST often necessitates multiple endpoints, GraphQL consolidates this need into a single query.
GraphQL representations are akin to objects, with each object possibly possessing numerous fields. This creates a hierarchy, especially useful when interacting with more complex datasets.
Queries consist of one or more fields. Each field can be an object, a scalar value, or an array of other objects or scalar values.
Structurally, a field within a query resembles an object property in JavaScript or a dictionary entry in Python.
Fields can maintain a one-to-one mapping with backend resources. However, they can also refer to themselves. This self-reference is invaluable for processing hierarchical data structures.
A simple example is a tree, where each node can potentially have multiple child nodes.
GraphQL exploits self-referential fields to enable powerful data relationships, such as nodes having other nodes as their children.
Here is the JSON representation:
{
"query": {
"user": {
"id": 123,
"name": "John Doe"
}
}
}
And here is the corresponding algorithm:
function fetchData(query) {
const result = {};
for (let field in query) {
if (isValidField(field)) {
result[field] = fetchData(query[field]);
}
}
return result;
}
Here are the JSON representation and the corresponding Python algorithm:
{
"query": {
"user": {
"id": 123,
"posts": {
"title": "GraphQL Basics"
}
}
}
}
def fetch_user_data(user_id, query):
user_data = fetch_user_from_db(user_id)
result = {}
for field, value in user_data.items():
if field in query:
if isinstance(value, dict):
result[field] = fetch_user_data(user_id, query[field])
else:
result[field] = value
return result
GraphQL brings a robust type system that ensures clear data structures and reliable schema enforcement. Let's take a closer look at key concepts like Scalars, Enums, and Input Types.
- Scalar: Represents a single value, such as a string or number.
- Object: Defines a structured collection of fields, each with its type.
- List: Indicates an array of values of the specified type.
- Enum: Offers a predefined set of possible values.
- Union: Represents a selection of object types, yet only allows querying common fields.
- Interface: Guarantees certain fields on all possible implementing types.
Here is Schema Definition Language (SDL):
- For a scalar type
scalar DateTime
- For an object type
type Author {
id: ID!
name: String
books: [Book]
}
And here is the complete schema with Query and Mutation:
type Query {
author(id: ID!): Author
allAuthors: [Author]
}
type Mutation {
addAuthor(name: String!): Author
}
type Book {
title: String
author: Author
}
Here is schema using Enums:
enum Episode {
NEW_HOPE
EMPIRE
JEDI
}
type Character {
name: String!
appearsIn: [Episode!]!
}
And here is an example using Union Types:
type Human {
name: String!
height(unit: LengthUnit = METER): Float
}
type Droid {
id: String!
name: String!
}
union SearchResult = Human | Droid
Here is a code example using Scalars and Input Types:
- For a scalar input:
input DateRange {
start: DateTime
end: DateTime
}
- For a custom input type:
type Query {
getPosts(publishedAfter: DateRange): [Post]
}
In GraphQL, developers use two main types of operations to interact with data, known as Queries and Mutations.
- Purpose: Read data from the server.
- HTTP Equivalent: Usually
GET
. - Immutability: Queries are Immutable; they don't change server data.
- Requesting Specific Fields: Allows clients to fetch only the data they need, increasing efficiency.
- Caching: Results can be cached since queries remain consistent unless modified by the client.
- Errors: Fails if the server cannot fulfill the data request.
- Purpose: Modify data on the server.
- HTTP Equivalent: Typically
POST
orPUT
. - Immutability: Mutations are Mutable; they trigger data changes.
- Requesting Specific Fields: Can include a return payload of specific fields for client updates post-mutation.
- Caching: Results are typically not cacheable.
- Errors: Can indicate both successes and failures, and usually carry specific feedback.
When integrating GraphQL, data retrieval is performed through a query.
-
Define the Query: GraphQL queries specify the exact shape and structure of the expected response using the same dataset as the server schema.
-
Execute the Query: Queries are typically triggered via an HTTP POST request to the server's endpoint.
-
Handle the Response: Expect the exact data structure that you requested, with no need for subsequent requests or data manipulation. errors, and then receive the data in the precise shape you requested.
query {
bookById(id: "4") {
title
author {
name
age
}
}
}
GraphQL augments traditional data modeling by incorporating its unique concept of scalars, which represent singular, atomic data types.
- Int: A signed 32-bit integer.
- Float: A double-precision floating-point number.
- String: A sequence of characters.
The Boolean type, representing true or false, is also one of the core scalars.
Developers can define custom data types using custom scalars to match specific requirements. For instance, Date
might be a developer-defined custom scalar that conforms to date-time values.
GraphQL provides easy extensibility through custom scalars to cater to diverse data types not covered by its core set or to enforce data consistency.
- Uniformity: Scalars ensure consistent data representation across diverse clients.
- Ease of Extension: Custom scalars let you unleash flexibility within GraphQL, tailoring data types to your unique needs.
- Efficiency: Since each query selects precisely what's needed, reduced data transmission decreases loading time and conserves bandwidth.
Resolvers in GraphQL tie specific fields of a schema to the actual functions that resolve these fields. They are critical for data fetching and manipulation.
- Data Fetching: The primary role of resolvers is to fetch the data for the fields they are responsible for.
- Data Transformation: Resolvers can perform any necessary transformations on the data before returning it to the client.
- Data Source Specification: Resolvers detail where to retrieve the data from (such as a database or an API).
- Single Responsibility: A resolver is responsible for a specific field in the schema.
- Parent-Child Relationships: Resolvers can "pass" data between related fields, e.g., a user resolver can return details about a user's posts.
- Root Resolvers: Deal with top-level query and mutation fields.
- Nested Resolvers: Handle specific fields on related types.
Here is the GraphQL Schema:
type Query {
user(id: Int!): User
}
type User {
id: Int!
name: String!
posts: [Post]
}
type Post {
id: Int!
title: String!
content: String!
}
And here is the resolver code in JavaScript:
const resolvers = {
Query: {
user: (parent, { id }, context, info) => fetchUser(id),
},
User: {
posts: (parent, args, context, info) => fetchPostsForUser(parent.id),
},
};
// Sample data for illustration
const sampleData = {
users: [
{ id: 1, name: "John" },
{ id: 2, name: "Jane" },
],
posts: [
{ id: 1, title: "First Post", content: "Hello, first post!", userId: 1 },
],
};
function fetchUser(id) {
return sampleData.users.find((user) => user.id === id);
}
function fetchPostsForUser(userId) {
return sampleData.posts.filter((post) => post.userId === userId);
}
In this example, when a client sends a query for a user's posts, the posts
resolver is invoked and calls fetchPostsForUser
to retrieve the posts associated with that user.
GraphQL offers many advantages over traditional REST APIs, ranging from optimized client-server communication to improved developer experience.
- Selectivity: GraphQL allows clients to specify the exact fields they need, reducing data over-fetching.
- No Multiple Endpoints: Instead of hitting multiple endpoints for diverse data, clients can make a single request.
- Strongly Typed Schema: GraphQL ensures data consistency by defining a schema, a feature not inherent in REST.
- Structured Endpoints: REST APIs can alter data formats and required attributes, making assumptions about endpoint responses more error-prone.
- Client Needs Alignment: GraphQL aligns server data with client requirements, promoting a more tailored approach for each client.
- Mitigated Under-Fetching: REST can suffer from under-fetching, prompting multiple requests for comprehensive data.
- Streamlined Data Collection: GraphQL, on the other hand, accesses interconnected entities and their data with one consolidated request.
- Socket-Like Experience: Integrated subscriptions in GraphQL allow for real-time updates, a feature often necessitating additional effort in REST.
- Unified Backend: With GraphQL, you can amalgamate data from various sources into a single endpoint, a setup that's more challenging with other languages like SQL.
- Comprehensive API: GraphQL has a robust toolset, including playgrounds that offer live interaction.
- Inherent Documentation: Its introspective nature ensures self-documenting APIs, a feature often less prominent in other query languages.
- Control Over Response: Clients have unparalleled influence over the response format, something less straightforward in languages like SQL.
- Data Manipulation: With GraphQL, you can extrapolate data for specific tasks, going beyond the typical "get" operations.
- Domain-Driven Applications: GraphQL is effective in presenting the domain model to the user, enhancing user interaction and comprehension.
In GraphQL, queries represent requests for specific data fields. To refine these requests, you can use arguments with fields.
To pass arguments, include them within the parentheses next to the field name. Each argument is defined using its unique name followed by the colon :
and the intended value.
{
user(id: "XYZ_123") {
name
email
}
}
Here is the GraphQL schema:
type Query {
booksByAuthor(author: String, count: Int): [Book]
}
And here is the query:
{
booksByAuthor(author: "JK Rowling", count: 3) {
title
}
}
In GraphQL, a fragment enables describing a set of fields that can be repeatedly used across multiple queries.
- Reusability: Easily reuse identical fields in various queries.
- Modularity: Enhance code organization by separating logical groupings.
- Clarity: Increase readability by outlining expected data needs in one place.
Fragments are crafted using the fragment
keyword and must specify a name along with the desired fields. They are scoped to a particular type.
fragment UserInfo on User {
id
name
email
}
To incorporate a fragment into a query, you utilize the ...
(spread) syntax followed by the fragment name.
query GetUserAndTheirPost {
user(id: "123") {
...UserInfo
}
postsForUser(id: "123") {
...PostInfo
}
}
While fragments help with code modularity and repeated field lists, GraphQL servers are smart enough to deduplicate and combine fields from all sources, ensuring minimal overhead in the underlying data fetch.
Queries can reference multiple fragments, and the fields from all fragments get composed in the query, streamlining data handling.
query GetMyData {
myself {
...BasicUserInfo
...ExtendedUserInfo
...UserAddressInfo
}
}
Sometimes, clients might have unique field requirements that the server's general schema doesn't cover. For this, client-specific fragments offer an avenue to define custom fields tailored to the client's needs.
fragment DisplayUserInfo on User {
... on RegularUser {
subscriptionStatus
lastLogin
securityQuestions
}
... on AdminUser {
permissions
staffSince
}
}
In contrast to globally defined fragments, client-specific fragments cater to the specific needs of the query or operation where they're referenced.
Data Federation in GraphQL, achieved through resolvers, instigates caching at multiple levels to heighten efficiency and optimize data handling.
- Purpose: Minimize network traffic and local state management.
- Mechanism:
Apollo Client
offers automatic in-memory caching. This ensures that redundant requests such as querying the same data multiple times don't hit the server.
- Purpose: Amplify efficiency by using explicitly stated HTTP caching directives.
- Mechanism: The caching mechanism of the server is dependent on the HTTP cache-control headers. If data is cacheable, these headers specify how long it should be kept in the cache.
- Purpose: Establish a global and persistent cache for recurrent data requests.
- Mechanism: Beyond the scope of GraphQL per se, several solutions like Redis for in-memory caching or Apache Ignite as a distributed cache facilitate data storage, minimizing the need for repeated remote calls.
GraphQL employs strategies to ensure coherence across the various cache levels:
- Real-Time Feed: With subscriptions, a server can alert the client about new data, prompting cache updates.
- Partial Updates: Through
GraphQL
subscriptions, you can tailor the response to only include changed or updated information, preventing a full cache refresh. - Optimistic UI and cache updates:
Apollo Client
manages client-side cache updates, providing immediate UI feedback. Updates are subsequently synched with the server, ensuring coherence.
Here is the GraphQL SDL
schema definitions:
type Query {
# Utilizes defaults from server config
getUser(id: ID!): User @cacheControl
# A specific request with an assigned cache time
getPost(id: ID!): Post @cacheControl(maxAge: 60)
# Bypasses cache directives
getMessage(id: ID!): Message @cacheControl(maxAge: 0)
}
type User {
id: ID!
name: String!
email: String!
}
type Post {
id: ID!
title: String!
content: String!
}
type Message {
id: ID!
text: String!
}
directive @cacheControl(
maxAge: Int
scope: CacheScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE