Skip to content

Migrating from REST to GraphQL

Introduction

GraphQL has been gaining a lot of traction with enterprises and startups for their application data layers. Historically, the web has been built using REST and SOAP APIs which have served their purpose successfully for years, but as applications have gotten more complicated and data has become richer, these solutions have created friction in developing performant software quickly.

In this article, we'll briefly discuss some of the problems with traditional API solutions, the benefits of migrating to GraphQL, and the strategy for migrating to a GraphQL solution.

Traditional API Problems

In traditional API systems, we typically suffer from a few common issues:

  1. Data under-fetching or n+1 fetching
  2. Data over-fetching
  3. All-or-nothing Responses
  4. Lack of batch support

Data Under-fetching

Traditional resources require us to request data on a per-entity basis, e.g. only users or only posts. For example, using REST, if we want to get some user details and their posts, we'd have to make the following requests:

  1. GET /users/1
  2. GET /users/1/posts

Data Over-fetching

Conversely, when we request certain data, it will give us all the available information including data we might not care about. From our previous example, we might only want a user's name and username but the response might provide us their creation time and bio.

All-or-nothing Responses

However, if there's an error somewhere in this process, we might not get any data. Instead, we receive an HTTP status code informing us of a failure with an error message but none of the data that was fetchable.

Lack of Batch Support

Finally, for our more complex page, we might need to run multiple requests that can be parallelized but traditional APIs don't support this behavior out of the box. Dashboards, for example, might need sales and marketing data which will require our clients to make two separate requests to our server and wait on results before displaying that data causing perceived slowness in our application.

The GraphQL Advantage

Out of the box, GraphQL solves all of these described issues due to its declarative querying syntax and data handling. When you fetch data, you can request the exact data you need, and using the connection among entities, you can retrieve those relationships in a single request. If any of the data fails to fetch, GraphQL will still tell you about the data that was successfully retrieved and about the failures in fetching the other data, allowing you to show your users data regardless of failures. GraphQL also allows you to group multiple operations in a single request and fetch all data from a single request, thus reducing the number of round trips to your server and increasing perceived speed of your application.

In addition to these features, GraphQL creates a single gateway for your clients, reducing friction in team communication around how data should be fetched. Your API is now abstracted away behind a single endpoint that also provides documentation on how to use it.

GraphQL Architecture

Given all these advantages, it's no wonder teams are moving to GraphQL, but it leaves the question of: how?

Migration Strategy

The GraphQL migration strategy is incremental so you don't have to slow down development to port over existing data or endpoints until you're ready to opt into those changes.

0. Before you begin

Before you start migration, here are some suggestions to think about as you're building new features or modifying the system in any way.

Don't build any new REST endpoints. Any new REST work is going to be additional GraphQL work later. Do yourself a favor and build it in GraphQL already.

Don't maintain your current REST endpoints. Porting REST endpoints to GraphQL is simple and GraphQL will provide you more functionality to build the exact behavior you want.

Leverage your existing REST endpoints to prototype quickly. You can use your existing REST API to power your GraphQL implementation. This won't be sustainable or performant long term, but it's a great way to get started.

1. Pick your GraphQL Implementation

Apollo and Relay are the two most popular fullstack GraphQL solutions, but you can also build your own solutions. Regardless of what you use, you'll use this to implement your server endpoint and connect to it with your client. All GraphQL requests go through a single endpoint, so once this is up and running, you can connect to it and begin porting functionality.

2. Select your first feature to build or port

With our server, we can start adding to it. Following our earlier example, let's migrate user posts.

3. Define your schema types

Now that we've decided on user posts, we have two routes here: (1) migrate users and posts or (2) migrate posts with a filter on user. For this, we're going to migrate posts and filter on user ID for now. To start, we'll define our post type in the schema and define its query type:

type Post {
  id: ID!
  userId: ID!
  content: String!
}

type Query {
  posts(userId: ID): [Post]
}

We now have a Post type that has an id and content and knows which user it belongs to. Additonally, we have a query called Posts that optionally accepts a userId as a filter and returns a list of Posts. It's important to note that it is semantically incorrect in GraphQL to expose the userId as a field. Instead, we should connect a post to its user and expose that entity relation, but those will be choices you make as you design your API.

4. Build our data resolver

Now, we need to connect our schema type and query to our data. For this, we'll use a resolver. The following syntax will vary slightly pending your server implementation, but using JavaScript and the GraphQL specification, we'd end up with the following resolver object:

const fetch = require('node-fetch');

export const resolvers = {
  Query: {
    posts: async (obj, args, context) => {
      const { API_URL } = process.env;
      const { userId } = args;

      if (userId){
        const response = await fetch (`${API_URL}/users/${userId}/posts`);
        return await response.json();
      }

      const response = await fetch (`${API_URL}/posts`);
      return await response.json();
    },
  }
};

If the userId is present in the query arguments, we use our existing REST API to fetch the posts by user, but if no userId is provided, we use the posts route directly. Now, we can make the following request on the frontend to retrieve our data:

query UserPosts($userId: ID!) {
  posts(userId: $userId) {
    id
    content
  }
}

I chose to use node-fetch for my implementation because it was simple, but you can use any HTTP library of your choice. However, if you're in the Apollo ecosystem, they've built a RESTDataSource library that will create an extension to your GraphQL implementation for handling resolvers to microservice APIs that can setup the boilerplate for that service so you only worry about fetching the data.

5. Next Steps

Extending Our Graph

Now that we have our data integrated, we need to complete the graph by connecting related types. Instead of Post having a userId, it can have a User and fetch the author details directly from the same query, e.g.

query UserPosts($userId: ID!) {
  posts(userId: $userId) {
    id
    content
    user {
      id
      avatarUrl
      displayName
    }
  }
}

Monoliths

Because we now have queries and types with full control of our schema, we can update our resolver functionality to rely on the codebase and not our REST API abstraction which will give us some added perfromance benefits. We can keep stitching together new types and extend our API further.

Microservices

GraphQL and microservices go hand-in-hand pretty well. GraphQL supports schema stitching, which allows us to build individual GraphQL APIs in our microservices and then combine them to make up our larger interface. Now, instead of configuring our clients to define all the different connections to different services, our GraphQL server understands where to collect all the data from, simplifying the amount of information the frontend needs to know about in order to complete requests.

Performance

A major downside to GraphQL can be the server-side overfetching, or n+1 problem. Because GraphQL doesn't know exactly how data is structured in the database, it cannot optimize for redundant requests in the graph tree. However, the GraphQL DataLoader library is here to solve exactly that. It determines any data that's already been fetched and caches for use in any sub-query to follow.

Conclusion

With all this power, it's no wonder GraphQL is picking up so much steam in the community. That being said, GraphQL isn't for everyone or might not be a good solution for your team today. However, I would suspect lots of future APIs we rely upon will start utilizing GraphQL more heavily and we'll see a trend away from traditional REST. Hopefully, you've seen the opportunity of GraphQL in your codebase and how it will help your team deliver quality products faster, and you can have a conversation with your team about a possible migration.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Angular 17: Continuing the Renaissance cover image

Angular 17: Continuing the Renaissance

Angular 17: A New Era November 8th marked a significant milestone in the world of Angular with the release of Angular 17. This wasn't just any ordinary update; it was a leap forward, signifying a new chapter for the popular framework. But what made this release truly stand out was the unveiling of Angular's revamped website, complete with a fresh brand identity and a new logo. This significant transformation represents the evolving nature of Angular, aligning with the modern demands of web development. To commemorate this launch, we also hosted a release afterparty, where we went deep into its new features with Minko Gechev from the Angular core team, and Google Developer Experts (GDEs) Brandon Roberts, Deborah Kurata, and Enea Jahollari. But what exactly are these notable new features in the latest version? Let's dive in and explore. The Angular Renaissance Angular has been undergoing a significant revival, often referred to as Angular's renaissance, a term coined by Sarah Drasner, the Director of Engineering at Google, earlier this year. This revival has been particularly evident in its recent versions. The Angular team has worked hard to introduce many new improvements, focusing on signal-based reactivity, hydration, server-side rendering, standalone components, and migrating to esbuild and Vite for a better and faster developer experience. This latest release, in particular, marks many of these features as production-ready. Standalone Components About a year ago, Angular began a journey toward modernity with the introduction of standalone components. This move significantly enhanced the developer experience, making Angular more contemporary and user-friendly. In Angular's context, a standalone component is a self-sufficient, reusable code unit that combines logic, data, and user interface elements. What sets these components apart is their independence from Angular's NgModule system, meaning they do not rely on it for configuration or dependencies. By setting a standalone: true` flag, you no longer need to embed your component in an NgModule and you can bootstrap directly off that component: `typescript // ./app/app.component.ts @Component({ selector: 'app', template: 'hello', standalone: true }) export class AppComponent {} // ./main.ts import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent).catch(e => console.error(e)); ` Compared to the NgModules way of adding components, as shown below, you can immediately see how standalone components make things much simpler. `ts // ./app/app.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], }) export class AppComponent { title = 'CodeSandbox'; } // ./app/app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule ], providers: [], bootstrap: [AppComponent] }) export class AppModule { } // .main.ts import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic() .bootstrapModule(AppModule) .catch((err) => console.error(err)); ` In this latest release, the Angular CLI now defaults to generating standalone components, directives, and pipes. This default setting underscores the shift towards a standalone-centric development approach in Angular. New Syntax for Enhanced Control Flow Angular 17 introduces a new syntax for control flow, replacing traditional structural directives like ngIf` or `ngFor`, which have been part of Angular since version 2. This new syntax is designed for fine-grained change detection and eventual zone-less operation when Angular completely migrates to signals. It's more streamlined and performance-efficient, making handling conditional or list content in templates easier. The @if` block replaces `*ngIf` for expressing conditional parts of the UI. `ts @if (a > b) { {{a}} is greater than {{b}} } @else if (b > a) { {{a}} is less than {{b}} } @else { {{a}} is equal to {{b}} } ` The @switch` block replaces `ngSwitch`, offering benefits such as not requiring a container element to hold the condition expression or each conditional template. It also supports template type-checking, including type narrowing within each branch. ```ts @switch (condition) { @case (caseA) { Case A. } @case (caseB) { Case B. } @default { Default case. } } ``` The @for` block replaces `*ngFor` for iteration and presents several differences compared to its structural directive predecessor, `ngFor`. For example, the tracking expression (calculating keys corresponding to object identities) is mandatory but offers better ergonomics. Additionally, it supports `@empty` blocks. `ts @for (item of items; track item.id) { {{ item.name }} } ` Defer Block for Lazy Loading Angular 17 introduces the @defer` block, a dramatically improving lazy loading of content within Angular applications. Within the `@defer` block framework, several sub-blocks are designed to elegantly manage different phases of the deferred loading process. The main content within the `@defer` block is the segment designated for lazy loading. Initially, this content is not rendered, becoming visible only when specific triggers are activated or conditions are met, and after the required dependencies have been loaded. By default, the trigger for a `@defer` block is the browser reaching an idle state. For instance, take the following block: it delays the loading of the calendar-imp` component until it comes into the viewport. Until that happens, a placeholder is shown. This placeholder displays a loading message when the `calendar-imp` component begins to load, and an error message if, for some reason, the component fails to load. `ts @defer (on viewport) { } @placeholder { Calendar placeholder } @loading { Loading calendar } @error { Error loading calendar } ` The on` keyword supports a wide a variety of other conditions, such as: - idle` (when the browser has reached an idle state) - interaction` (when the user interacts with a specified element) - hover` (when the mouse has hovered over a trigger area) - timer(x)` (triggers after a specified duration) - immediate` (triggers the deferred load immediately) The second option of configuring when deferring happens is by using the when` keyword. For example: `ts @defer (when isVisible) { } ` Server-Side Rendering (SSR) Angular 17 has made server-side rendering (SSR) much more straightforward. Now, a --ssr` option is included in the `ng new` command, removing the need for additional setup or configurations. When creating a new project with the `ng new` command, the CLI inquires if SSR should be enabled. As of version 17, the default response is set to 'No'. However, for version 18 and beyond, the plan is to enable SSR by default in newly generated applications. If you prefer to start with SSR right away, you can do so by initializing your project with the `--ssr` flag: `shell ng new --ssr ` For adding SSR to an already existing project, utilize the ng add` command of the Angular CLI: `shell ng add @angular/ssr ` Hydration In Angular 17, the process of hydration, which is essential for reviving a server-side rendered application on the client-side, has reached a stable, production-ready status. Hydration involves reusing the DOM structures rendered on the server, preserving the application's state, and transferring data retrieved from the server, among other crucial tasks. This functionality is automatically activated when server-side rendering (SSR) is used. It offers a more efficient approach than the previous method, where the server-rendered tree was completely replaced, often causing visible UI flickers. Such re-rendering can adversely affect Core Web Vitals, including Largest Contentful Paint (LCP), leading to layout shifts. By enabling hydration, Angular 17 allows for the reuse of the existing DOM, effectively preventing these flickers. Support for View Transitions The new View Transitions API, supported by some browsers, is now integrated into the Angular router. This feature, which must be activated using the withViewTransitions` function, allows for CSS-based animations during route transitions, adding a layer of visual appeal to applications. To use it, first you need to import withViewTransitions`: `ts import { provideRouter, withViewTransitions } from '@angular/router'; ` Then, you need to add it to the provideRouter` configuration: `ts bootstrapApplication(AppComponent, { providers: [ provideRouter(routes, withViewTransitions()) ] }) ` Other Notable Changes - Angular 17 has stabilized signals, initially introduced in Angular 16, providing a new method for state management in Angular apps. - Angular 17 no longer supports Node 16. The minimal Node version required is now 18.13. - TypeScript version 5.2 is the least supported version starting from this release of Angular. - The @Component` decorator now supports a `styleUrl` attribute. This allows for specifying a single stylesheet path as a string, simplifying the process of linking a component to a specific style sheet. Previously, even for a single stylesheet, an array was required under `styleUrls`. Conclusion With the launch of Angular 17, the Angular Renaissance is now in full swing. This release has garnered such positive feedback that developers are showing renewed interest in the framework and are looking forward to leveraging it in upcoming projects. However, it's important to note that it might take some time for IDEs to adapt to the new templating syntax fully. While this transition is underway, rest assured that you can still write perfectly valid code using the old templating syntax, as all the changes in Angular 17 are backward compatible. Looking ahead, the future of Angular appears brighter than ever, and we can't wait to see what the next release has in store!...

Bun v1.0 cover image

Bun v1.0

On September 8, 2023, Bun version 1 was released as the first production-ready version of Bun, a fast, all-in-one toolkit for running, building, testing, and debugging JavaScript and TypeScript. Why a new JS runtime You may ask, we already have Node and Deno, so why would we need another javascript runtime, Well yes we had Node for a very long time, but developers face a lot of problems with it, and maybe the first problem is because it’s there for a very long time, it has been changing a lot between different versions and one of the biggest nightmares for JavaScript developers these days is upgrading the node version. Also, Node lacks support for Typescriptt. Zig programming language One of the main reasons that Bun is faster than Node, is the programming language it has been built with which is Zig. Zig is a very fast programming language, even faster than C) (here is some benchmarks), it focuses on performance and memory control. The reasons it’s faster than C is because of the LLVM optimizations it has, and also the way it handles the undefined behavior under the hood Developer Experience Bun delivers a better developer experience than Node on many levels. First, it’s almost fully compatible with Node so you can use Node packages without any issues. Also, you don’t need to worry about JS Common and ES Modules anymore, you can use both in the same file, yup you read that right, for example: `js import { useState } from 'react'; const React = require('react'); ` Also, it has a built-in test framework similar to Jest or Vitest in the project so no need to install a different test framework with different bundler in the same project like Webpack or Vite `js import { describe, expect, test, beforeAll } from "bun:test"; ` Also, it supports JSX out-of-the-box `bash bun index.tsx ` Also, Bun has the fastest javascript package manager and the most efficient you can find as of the time of this post `bash bun install ` Bun Native APIs Bun supports the Node APIs but also they have fun and easy APIs to work with like `Bun.serve()` : to create HTTP server `Bun.file()` : to read and write the file system `Bun. password.hash()`: to hash passwords `Bun.build()`: to bundle files for the browser `Bun.FileSystemRouter()`: a file system router And many more features Plugin system Bun also has an amazing plugin system that allows developers to create their own plugins and add them to the Bun ecosystem. `js import { plugin, type BunPlugin } from "bun"; const myPlugin: BunPlugin = { name: "Custom loader", setup(build) { // implementation }, }; ` Conclusion Bun is a very promising project, and it’s still in the early stages, but it has a lot of potential to be the next big thing in the JavaScript world. It’s fast, easy to use, and has a lot of amazing features. I’m very excited to see what the future holds for Bun and I’m sure it will be a very successful project....

Starter.dev: Bootstrap your project with zero configuration! cover image

Starter.dev: Bootstrap your project with zero configuration!

Starter.dev: Bootstrap your project with zero configuration! Table of contents - Why Starter Kits? - Why Showcases? - Getting Started - What Kits Exist Today? - Collaborate with Us - What’s Next? We’re excited to present you with starter.dev, a set of zero-configuration project kits built with your favorite tools. Each kit is configured with the following so you can focus on building features instead of spending time on configuration: - State Management - Data Fetching - Testing - Storybook - Community standardized ESLint and Prettier We’re also launching starter.dev showcases which utilizes the kits and demonstrates how to build at-scale applications and go beyond the basic TodoMVC app, though we love a good TodoMVC! Why Starter Kits? This Dot Labs engages with its clients to build modern applications, and many of them are requesting greenfield projects using the latest and greatest technologies. While starting these projects, our architects found that they were repeating a bunch of the same steps each time they needed to start a new one. Most meta frameworks ship with the bare minimum, and don’t include features like testing or Storybook out of the box, and configuring these technologies can be time consuming. With this challenge in mind, we sought to create zero-config starter_ templates to help kick _start_ projects. And thus, starter.dev was born! Why Showcases? During starter.dev’s development, Minko Gechev from the Angular team approached us about a project to help enhance Angular education tooling. You can learn more about this specific effort in the blog post announcing the GitHub Clone Showcases. Minko’s idea was to demonstrate building applications that utilize these key features of the Angular framework: Routing Forms State Management API interactions - REST or GraphQL Authentication This idea laid the groundwork for many of the starter kits we created. We’ve developed several GitHub Clone showcase to help developers understand how to best utilize the kits, and build at-scale applications that accompany our kits. Getting Started Getting started with starter.dev (pun intended) is as simple as running a scaffolding script: - Run npx @this-dot/create-starter` to run the scaffolding tool - Select one of the kits from our library from the CLI - Name your project - cd` into your project directory, install dependencies using the tool of your choice. After completing these steps, you can start building features in your new project immediately. What Kits Exist Today? This Dot is happy to ship starter.dev with the following kits: - Angular + Apollo Client + Tailwind CSS - Angular + NgRx + SCSS - Create React App + RxJS + Styled Components - Next.js + TanStack Query (formerly React Query) + Tailwind CSS - Remix + GraphQL + Tailwind CSS - Vue 3 + Apollo Client + Quasar - Qwik + GraphQL + Tailwind CSS - SvelteKit + SASS Each kit ships with the following out-of-the-box: - Testing via jest or vitest - Storybook - ESLint and Prettier Configuration - State Management - Data Fetching for either REST or GraphQL - Some starter components to demonstrate global state management and data fetching These starter kits don’t ship with E2E testing libraries, such as Cypress or Playwright, for now, because these tools come with amazing out-of-the-box configurations and do not require additional setups. However, the showcases use Cypress tests consistency which you can check out in the showcases repo. Collaborate with us Starter.dev began as an internal need, but anyone can benefit from the existence of these kits. While there is a set structure for building out new kits, This Dot welcomes requests for new kits. We’ll work with you to determine what the structure of your kit should be and then scaffold out all of the issues needed to complete the work. Our team will help build out new issues, but we encourage the community to jump in and help build as well. This is a great opportunity to collaborate with the community as both a learner and a teacher. At This Dot, two of our guiding principles are: Getting Better Together and Giving Back to the Community. We believe that starter.dev is a perfect opportunity for us to get better through collaborative efforts with community members, and helping the community through what we hope is a great open source resource. What’s Next? We want to get this tool into your hands, and improve what exists. Tell us what is working for you and not, and we’ll do our best to address those issues. Next, we want to expand our library of kits. Tell us what are some kits you would like to see. We’re looking into building out kits for SolidJS and Node backend technologies as part of our next iterations, but we’re sure there other tools people would like to see. Finally, we’ll be publishing some additional educational materials around some of the decisions and design patterns we’ve chosen for each kit and showcase. We’re excited to share our process with you....

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry cover image

Software Team Leadership: Risk Taking & Decision Making with David Cramer, Co-Founder & CTO at Sentry

In this episode of the engineering leadership series, Rob Ocel interviews David Cramer, co-founder and CTO of Sentry, delving into the importance of decision-making, risk-taking, and the challenges faced in the software engineering industry. David emphasizes the significance of having conviction and being willing to make decisions, even if they turn out to be wrong. He shares his experience of attending a CEO event, where he discovered that decision-making and conflict resolution are struggles even for successful individuals. David highlights the importance of making decisions quickly and accepting the associated risks, rather than attempting to pursue multiple options simultaneously. He believes that being decisive is crucial in the fast-paced software engineering industry. This approach allows for faster progress and adaptation, even if it means occasionally making mistakes along the way. The success of Sentry is attributed to a combination of factors, including market opportunity and the team's principles and conviction. David acknowledges that bold ideas often carry a higher risk of failure, but if they do succeed, the outcome can be incredibly significant. This mindset has contributed to Sentry’s achievements in the industry. The interview also touches on the challenges of developing and defending opinions in the software engineering field. David acknowledges that it can be difficult to navigate differing viewpoints and conflicting ideas. However, he emphasizes the importance of standing by one's convictions and being open to constructive criticism and feedback. Throughout the conversation, David emphasizes the need for engineering leaders to be decisive and take calculated risks. He encourages leaders to trust their instincts and make decisions promptly, even if they are uncertain about the outcome. This approach fosters a culture of innovation and progress within engineering teams. The episode provides valuable insights into the decision-making process and the challenges faced by engineering leaders. It highlights the importance of conviction, risk-taking, and the ability to make decisions quickly in the software engineering industry. David's experiences and perspectives offer valuable lessons for aspiring engineering leaders looking to navigate the complexities of the field....