How we translated legacy project into GraphQL

Hi, Habr. My name is Anton Potapov, I’m an iOS developer at FINCH . Today I want to talk in detail about how to translate a mobile project into GraphQL, describe the pros and cons of this approach. Let's get started.



Brief introduction



"Legacy." I think everyone heard this terrible word, and most met him face to face. Therefore, you don’t need to tell how difficult it is to integrate new and great features into your project.



image



One of our projects is an application for a large lottery company (I just can’t give a name since the NDA). Recently, I needed to translate it into GraphQL without losing my mind.



The project is very large - at first glance, it was necessary to spend at least 200-300 hours, but this goes beyond all possible two-week sprints. We also could not allocate the whole sprint to just one task, as there were also side features, no less important than GraphQL.

We thought for a long time what to do and decided to translate the project step by step, model by model. With this approach, the transition will be smooth, and 200-300 hours will be distributed over several sprints.



Technical points



To work on the project, I used the Apollo library. There is already an article on Habré that describes all the subtleties of working with it, so I won’t repeat it again. I warn you in advance - working with a code-generated model is not very convenient and it is better to keep it as a "network". Each entity contains a __typename: String field, which, oddly enough, returns a type name.



Task decomposition



The first thing to do is to determine which Legacy model we will translate into GraphQL. In my case, it was logical to translate one of the massive GameInfo models, and DrawInfo embedded in it.





Having finished the choice, I decided to initialize the old legacy models with the new ones obtained with GraphQL. With this approach, it is not necessary to replace those parts of the application where the old model is used. Completing the previous models completely was risky - not the fact that the project would work as before, and it would take too much time.



In total, three stages of GraphQL implementation can be distinguished:





GraphQLClient



As with any client, GraphQLClient should have a fetch method, after which it will load the data we need.



In my opinion, the fetch method for GraphQLClient should accept an enum, based on which the corresponding request will be executed. This approach will allow us to easily receive data for the desired entity or even the screen. If the request accepts parameters, then they will be passed as associated values. With this approach, it is easier to use GraphQL and create flexible queries.



enum GraphQLRequest { case image(ImageId?) }
      
      





Our customized GraphQL client should contain ApolloClient customized for existing features, as well as the fetch loading method mentioned above.



 func fetch<Response>(requestType: GraphQLRequest, completion: @escaping (QLRequestResult<Response>) -> Void)
      
      





What is QLRequestResult? This is ordinary



  typealias QLRequestResult<Response> = Result<Response, APIError>
      
      





The fetch method allows you to access the client through the protocol and perform a switch on requestType. Depending on requestType, you can use the corresponding private loading method, where you can convert the resulting model to the old one. For example:



  private func fetchGameInfo<Response>(gameId: String? = "", completion: @escaping (QLRequestResult<Response>) -> Void) { //   Apollo  let query = GetLotteriesQuery(input: gameId) //       apolloClient.fetch(query: query, cachePolicy: .returnCacheDataAndFetch, queue: .global()) { result in switch result { case .success(let response): guard let gamesQL = response.data?.games, let info = gamesQL.info else { completion(.failure(.decodingError)) return } //    let infos: [Games.Info] = info.compactMap({ gameInfo -> Games.Info? in guard let gameIdRaw = gameInfo?.gameId, let gameId = GameId(rawValue: gameIdRaw), let bonusMultiplier = gameInfo?.bonusMultiplier, let maxTicketCost = gameInfo?.maxTicketCost, let currentDraws = gameInfo?.currentDraws else { return nil } let currentDrawsInfo = Games.Info.CurrentDrawsInfo(currntDraw: currentDraws) let gameInfo = Games.Info(gameId: gameId, bonusMultiplier: bonusMultiplier, maxTicketCost: maxTicketCost, currentDraws: currentDrawsInfo) return gameInfo }) //    let games = Games(info: infos) guard let response = games as? Response else { completion(.failure(.decodingError)) return } completion(.success(response)) case .failure(let error): … } } }
      
      





As a result, we get a ready-made, old model obtained from GraphQL.



Scalar type



The GraphQL documentation describes the scalar type as follows: "The scalar type is a unique identifier that is often used to re-select an object or as a key for the cache." For swift, a scalar type can easily be associated with typealias.



When writing a client, I ran into a problem that in my scheme there was a scalar type Long, which was essentially



 typealias Long = Int64
      
      





Everything would be fine, but Apollo automatically casts all user scalars to String, which causes a crash. I solved the problem like this:





In the future, for each scalar type, you need to add a new typealias. With Apollo version 14.0, they added "support for Int custom scalars." If you want to avoid using this approach and the flag in the codegen, then check out the solution to this problem in the Apollo git .



Advantages and disadvantages



Why is this approach good? A fairly quick transition to using GraphQL, relative to the approach with the removal of all old models.



In the future, when we need to get data for any screen / model, we can turn to GraphQLClient and get the necessary data.



Of the minuses:





findings



In general, switching to GraphQL can be quick and painless with a well-written client. It took me about three days = 24 working hours. During this time, I managed to create a client, translate the model and recreate the GameInfo model. The described approach is not a panacea, but a purely my decision, implemented in a short time.



If you have a GraphQL guru among you, I suggest sharing your experience and telling how convenient it is to use GraphQL + Apollo in large projects. Or is the game not worth the candle?



Thanks for your attention!



All Articles