Unlock the Next Level Elysia.js Performance with GraphQL Apollo #5
Imagine having a REST API endpoint with the same name and functionality, but serving two different clients. The first client only needs two fields, while the second requires twenty. This results in unnecessary data transfer and inefficient use of resources. In the other hand, this approach can lead to several issues such as Inefficient Data Transfer, Increased Complexity, Reduced Developer Productivity, Potential Security, and Suboptimal User Experience.
Therefore, GraphQL offers a clear and comprehensive description of your API’s data, allowing clients to request only the specific information they need. It simplifies API evolution and supports advanced developer tools. With GraphQL client can Ask for what you need, get exactly that.
Table of Contents:
A. When To Use
There are some reasons when you can implement GraphQL as the right choice:
- Dynamic Data Retrieval: Allow clients to fetch only the data they need, minimizing issues like over-fetching or under-fetching.
- Combined Resource Requests: Enable clients to combine multiple queries into one request, reducing network overhead.
- Live Data Updates: Ensure real-time data synchronization through GraphQL subscriptions.
- API Evolution Without Versions: Support frequent API updates and new features without breaking existing clients.
- Strong Typing and Documentation: Provide a well-defined contract between client and server through strong typing and auto-generated documentation.
- Handling Complex Relationships: Manage intricate data relationships with the ability to retrieve related data using a single query.
B. Call The Actions
I’ll explain the changes that will do in the scenario flow.
- Install the plugins
To install the plugins GraphQL, you can refer to the official documentation on Elysia.js in this link https://elysiajs.com/plugins/graphql-apollo
bun add graphql @elysiajs/apollo @apollo/server
2. Create a new table we can call it article
.
#schema.prisma
model User {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
email String @unique
password String @db.Text
created_at DateTime @default(now())
updated_at DateTime @default(now())
deleted_at DateTime?
file_avatar String? @db.Text
// Define the relation to the Article model
articles Article[]
}
model Article {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
name String
content String @db.Text
user_id String @db.Uuid
created_at DateTime @default(now())
updated_at DateTime @default(now())
deleted_at DateTime?
// Define the relation to the User model
user User @relation(fields: [user_id], references: [id])
@@index([user_id])
}
Run the migrations of the new table (article)
bunx prisma migrate dev --name create_articles_table
3. Create GraphQL Schema
In this schema, there are some type to defined such as user, article, query, and mutation.
const typeDefsGql = gql`
type User {
id: String
name: String
email: String
password: String
file_avatar: String
created_at: String
updated_at: String
deleted_at: String
articles: [Article]
}
type Article {
id: ID
name: String
content: String
user_id: ID
}
input ArticleInput {
name: String
content: String
user_id: ID
}
type Query {
getUsers: [User]
getArticles: [Article]
getUserByName(name: String): [User]
}
type Mutation {
createArticle(article: ArticleInput!): Article
deleteArticle(id: String!): Article
}
`;
The types User
and Article
are used as return values for the GraphQL queries getUsers
, getArticles
, and getUserByName
. Similarly, these types are also utilized in mutations such as createArticle
and deleteArticle
. In GraphQL, the Query
type is responsible for fetching data from the server, similar to a GET
request in REST APIs. Conversely, the Mutation
type handles data modifications like adding, updating, and deleting records on the server. Based on the schema, we had 3 method GET and 2 method for action changed data in the server. At the end, ArticleInput
is defined as structure of input data when performing operations like creating or updating an article in a GraphQL API.
Why use Input
in GraphQL? With input we can simplifies the mutations fields, ensure the client sends data in the correct format (type safety), and can be used across multiple mutations for consistency.
4. CRUD Sample GraphQL
First create a new file inside folder lib with name GqlPlugin.ts
. This file will be handle type schma of graphql and logic functions resolvers our graphql method based on schema. The completed code in that file is
import { apollo, gql } from "@elysiajs/apollo";
import { Article, PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const typeDefsGql = gql`
type User {
id: String
name: String
email: String
password: String
file_avatar: String
created_at: String
updated_at: String
deleted_at: String
articles: [Article]
}
type Article {
id: ID
name: String
content: String
user_id: ID
}
input ArticleInput {
name: String
content: String
user_id: ID
}
type Query {
getUsers: [User]
getArticles: [Article]
getUserByName(name: String): [User]
}
type Mutation {
createArticle(article: ArticleInput!): Article
deleteArticle(id: String!): Article
}
`;
const resolversGql = {
Query: {
getUsers: async () => {
return prisma.user.findMany({
include: {
articles: true,
},
});
},
getArticles: async () => {
return prisma.article.findMany({
include: {
user: true,
},
});
},
getUserByName: async (_parent: any, { name }: { name: string }) => {
return prisma.user.findMany({
where: {
name: {
contains: name,
mode: "insensitive",
},
},
});
},
},
Mutation: {
createArticle: async (_parent: any, { article }: { article: Article }) => {
const { name, content, user_id } = article;
return prisma.article.create({
data: { name, content, user_id },
});
},
deleteArticle: async (_parent: any, { id }: { id: string }) => {
return prisma.article.delete({
where: { id: id },
});
},
},
};
export { typeDefsGql, resolversGql };
Then, call it into main index.
app.use(
apollo({
typeDefs: typeDefsGql,
resolvers: resolversGql,
path: "/graphql-api"
})
);
When run the applications with bun run dev
based on the path we can access the apollo server like in the image below.
We can run our graphql in apollo server, or still used another applications like postman or insomnia which can make our informations more informative and easy to understand.
A. Query getUsers
B. getArticles
C. getUserByName
D. CreateArticle
E. deleteArticle
5. Auth Bearer GraphQL
To ensure that clients accessing the GraphQL API have a valid bearer token, additional configurations are required. For example, if the bearer token belongs to name admin on client, access should be granted. Conversely, if the client lacks a valid token, the system should deny access.
// apollo graphql (index.ts)
app
.use(
apollo({
typeDefs: typeDefsGql,
resolvers: resolversGql,
path: "/graphql-api",
context: async ({ request }: { request: Request }) => {
const authorization = request.headers.get("Authorization");
const split_auth = authorization?.split("Bearer ");
if (split_auth && split_auth[1] === "admin") {
return {
request,
authorization,
};
} else {
const error = new Error("Unauthorized");
throw error;
}
},
formatError: (err) => {
if (err.message === "Context creation failed: Unauthorized") {
// Set HTTP status code to 401
return {
message: err.message,
extensions: {
code: "UNAUTHORIZED",
},
};
}
return err;
},
})
);
The purposes of formatError
is to repharaphrase our default error from GraphQL to our custom error.
For full documentations check my works on my github repository here.
Thanks for read til the end, Let’s Explore It