Using Graphql for Modernizing Legacy Applications
I recently presented a tech talk about GraphQL and got a question about strategies for deprecating legacy monolithic applications and whether GraphQL could be used in that context. I thought it was a great question and here I am writing this article.
As applications age, they can become difficult to maintain and extend due to their complex and often outdated architectures. This can make it challenging for developers to build new features or integrations, and can also lead to poor performance and many other issues. One way to address these challenges is to modernize the application by updating its architecture and technologies to fulfill the new needs and make it more maintainable and extensible.
GraphQL is a query language for your API that allows clients to request exactly the data they need, and nothing more, without over-fetching or under-fetching. This can be a powerful tool for software modernization, as it provides a flexible and efficient way to access data and functionality through a single endpoint.
In this article, we’ll explore how you can use GraphQL to modernize a legacy application. We’ll start by looking at the benefits of using GraphQL on a software modernization, and then we’ll walk through a practical example of how to use GraphQL to modernize an existing application.
Benefits of Using GraphQL in a Software modernization
There are several benefits of using GraphQL in a software modernization project:
- Incremental Modernization: GraphQL can be added on top of an existing application without requiring a complete overhaul. This means that you can take advantage of the benefits of GraphQL without needing to do a full rewrite of your application.
- Flexibility: GraphQL allows clients to request exactly the data they need, and nothing more. This means that you can expose your application’s data and functionality in a uniform, predictable way, making it easier for developers to build new features and integrations.
- Efficiency: With GraphQL, the client specifies exactly what data it needs, and the server only returns that data. This can help improve the performance of the application, as it reduces the amount of data that needs to be transferred over the network.
- Improved Developer Experience: GraphQL provides a clear and intuitive way for developers to access and manipulate data, which can make it easier for them to work with the application.
- Microservices Integration: If your modernization project involves breaking down a monolithic application into microservices, GraphQL can be used to provide a consistent way for the different microservices to communicate with each other and with the client applications.
Additionally, GraphQL is a great tool for domain modeling and creating effective and domain driven APIs. REST Apis are resource based and it’s difficult to design your domain fluidly. GraphQL is schema driven technology and it’s graph based, giving you a more fluent ability of modeling your API schema effectively based on your domain needs, without having to expose any internal details or api implementation.
In domain modeling, the focus is on understanding and representing the core concepts and relationships in a specific domain (human resources platform, retail business, social media platform, etc.). This involves identifying the main entities (people, jobs, products, users, orders) and the relationships and interactions between them (a user can place multiple orders, an order can contain multiple products, a person can apply to multiple jobs, etc.).
Once the domain model has been defined, it can be easily translated to a GraphQL schema that exposes the data and functionality of the application in a structured and predictable way. For example, if the domain model includes a Product
entity and a User
entity, the GraphQL schema can express the easily express the relation between them. Queries and mutations can be easily defined on top of that, without the need of being resource specific.
Using GraphQL in conjunction with domain modeling help you ensure that the API is domain oriented, well-defined, organized, easy to understand, and able to evolve as the application grows. It can also make it easier for developers to work with the API, as they can use the domain model as a reference to understand the available data and functionality.
Overall, GraphQL can be a powerful tool for modernizing legacy applications making you put domain modeling at the center and also by providing a flexible, efficient, and declarative way to access data and functionality.
Show me the code!
Now that we’ve looked at the benefits of using GraphQL for software modernizations, let’s walk through an example of how to actually use it. For the purposes of this example, we’ll assume that we have a legacy monolithic application that exposes a REST API for retrieving user, orders and product information through different endpoints: /user, /order and /product.
The first step in adding GraphQL to our legacy application is to install the necessary dependencies. I’ll be using here graphql-yoga
and node-fetch
packages:
npm install graphql-yoga node-fetch
Next, we’ll define our schema based on our domain model. This will define the types and fields available in our API:
type Order {
id: String
user: User
products: [Product]
}
type Product {
id: String
name: String
description: String
price: Float
}
type User {
id: String
name: String
email: String
orders: [Order]
}
type Query {
user(id: String!): User
users: [User]
}
Now that we have our schema defined, let’s implement our resolvers:
const { GraphQLServer } = require("graphql-yoga");
const fetch = require("node-fetch");
const typeDefs = `
type Order {
id: String
user: User
products: [Product]
}
type Product {
id: String
name: String
description: String
price: Float
}
type User {
id: String
name: String
email: String
orders: [Order]
}
type Query {
user(id: String!): User
users: [User]
}
`;
const resolvers = {
User: {
orders: async parent => {
const response = await fetch(`https://some-legacy-app.com/orders?userId=${parent.id}`);
return response.json();
}
},
Order: {
products: async parent => {
// expect to receiver a list of productIds from order api
const response = await fetch(`https://some-legacy-app.com/products?ids=${parent.productIds.join(',')}`);
return response.json();
},
user: async parent => {
// expect to receiver an userId from order api
const response = await fetch(`https://some-legacy-app.com/user/${parent.userId}`);
return response.json();
}
},
Query: {
user: async (_, { id }) => {
const response = await fetch(`https://some-legacy-app.com/user/${id}/`);
return response.json();
},
users: async (_) => {
const response = await fetch(`https://some-legacy-app.com/user/`);
const results = await response.json();
return results['results']
}
}
};
const server = new GraphQLServer({ typeDefs, resolvers });
server.start(() => console.log("Server is running on localhost:4000"));
As our legacy API doesn’t return all information through one API (it’s resource based), we have to create custom resolvers to map orders and products to user. This way we have all information to fill our graphs and will return the exact information the user need.
This schema defines a User
type with three fields: id
, name
, email
and orders
and also defines two queries to return users. The user
field has an id
argument that can be used to specify the ID of the user to retrieve. The resolve
function is responsible for fetching the user from the legacy service using the provided ID.
Clients can send GraphQL queries to retrieve user data from the API. For example, a client could send the following query to retrieve a user with a specific ID:
{
user(id: "user-1") {
name
email
orders {
id
products {
name
}
}
}
}
And the server should return the requested data:
{
"data": {
"user": {
"name": "Gustavo de Lima",
"email": "gustavo@email.com",
"orders" : [
{
"id": "order-1",
"products" : [
{
"name": "Product A"
},
{
"name": "Product B"
}
]
}
]
}
}
}
Note that only the requested data is returned. Also, GraphQL will only request and return required data, so if we only want user name and email, GraphQL will not try to fetch order and products:
{
user(id: "user-1") {
name
email
}
}
And the server should return:
{
"data": {
"user": {
"name": "Gustavo de Lima",
"email": "gustavo@email.com"
}
}
}
Now we have GraphQL serving as a facade to our service. With incremental releases we can gradually point the resolvers to the new microsservices. For example, after having User data in a new microservice, the user resolver can now call the new API without having to change anything in the schema, keeping orders and products calling the legacy application and with zero harm to users.
Conclusion
In this article, we’ve looked at how GraphQL can be used to modernize a legacy application. By providing a flexible and efficient way to access data and functionality through a single endpoint, GraphQL can make it easier for developers to build new features and integrations, and can also improve the performance of the application. We’ve also walked through an example of how to write actual GraphQL code, demonstrating how it can be used to incrementally modernize an existing application.
I believe GraphQL has a lot to offer for software modernization projects, especially if your use case consists in breaking down a monolithic application or redesigning your domain model.