<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: IVAO</title>
    <description>The latest articles on DEV Community by IVAO (@ivao).</description>
    <link>https://dev.to/ivao</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9651%2F4a677d52-0c21-4d3b-bda3-4b65e679f0fc.png</url>
      <title>DEV Community: IVAO</title>
      <link>https://dev.to/ivao</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ivao"/>
    <language>en</language>
    <item>
      <title>Why We Moved API Authentication from the Gateway to Our Microservices</title>
      <dc:creator>David Tchekachev</dc:creator>
      <pubDate>Tue, 28 Oct 2025 15:05:05 +0000</pubDate>
      <link>https://dev.to/ivao/why-we-moved-api-authentication-from-the-gateway-to-our-microservices-d4n</link>
      <guid>https://dev.to/ivao/why-we-moved-api-authentication-from-the-gateway-to-our-microservices-d4n</guid>
      <description>&lt;p&gt;Moving our API authentication and authorization from gateway to microservice layer&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;There are various systems design to check if an API call can be processed, we recently moved that logic from our API gateway (aka &lt;em&gt;APIGW&lt;/em&gt;) to our microservices. &lt;br&gt;
The main reason being that our APIGW (&lt;a href="https://konghq.com/" rel="noopener noreferrer"&gt;kong&lt;/a&gt;) was facing some performance issues during auth validation, and so creating a bottleneck at the entry point of our infrastructure. &lt;/p&gt;

&lt;p&gt;After moving that logic into our microservices, our APIGW is now only focusing on routing the requests (which it does very well), while our APIs are scaling independently based on the load they receive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4f67xa5m3a3vd5eduirg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4f67xa5m3a3vd5eduirg.png" alt="IVAO API Architecture Change"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  A bit of context
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Preamble
&lt;/h3&gt;

&lt;p&gt;At &lt;a href="https://ivao.aero" rel="noopener noreferrer"&gt;IVAO&lt;/a&gt;, we built and operate a set of 15 microservice APIs written in NestJS, running on Kubernetes, served by &lt;a href="https://konghq.com/" rel="noopener noreferrer"&gt;kong&lt;/a&gt; as our API Gateway.&lt;/p&gt;

&lt;p&gt;Those API services serve over 200 separate endpoints under &lt;a href="https://api.ivao.aero" rel="noopener noreferrer"&gt;api.ivao.aero&lt;/a&gt;, which require some form of authentication (API Key or &lt;a href="https://www.jwt.io/introduction#what-is-json-web-token-structure" rel="noopener noreferrer"&gt;JWT&lt;/a&gt; Bearer token). Authentication and generic authorization checks were handled by kong, thanks to a &lt;a href="https://developer.konghq.com/custom-plugins/" rel="noopener noreferrer"&gt;Kong custom plugin&lt;/a&gt; written in Go, passing headers with needed information to upstream services. The configuration was done using &lt;a href="https://developer.konghq.com/kubernetes-ingress-controller/reference/custom-resources/#kongplugin" rel="noopener noreferrer"&gt;Kong CRD KongPlugin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are the most common access rules we have on our API endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is the request coming from a user's browser or an autonomous application/bot ?&lt;/li&gt;
&lt;li&gt;Is the request coming from an official IVAO website or from a 3rd-party ?&lt;/li&gt;
&lt;li&gt;Is the request coming from a staff user ?&lt;/li&gt;
&lt;li&gt;Is the request coming from a non-suspended user or application ?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One important information to keep in mind about the control of how our APIs are used:  &lt;strong&gt;we encourage all IVAO users to build on top of them!&lt;/strong&gt; Our goal is to expose as much as technically and legally possible for users to build the tools they want to. We even have a website (&lt;a href="https://developers.ivao.aero" rel="noopener noreferrer"&gt;developers.ivao.aero&lt;/a&gt;) where users can generate API Keys and OAuth2 credentials and start querying our APIs within minutes!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzban4zkdwzbmk4aomuix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzban4zkdwzbmk4aomuix.png" alt="IVAO Developers Website"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The &lt;em&gt;"issues"&lt;/em&gt; we were facing
&lt;/h3&gt;

&lt;p&gt;The main issue we were facing was was the plugin’s ability to handle high load. During peak times (typically weekends at IVAO), Kong was processing over &lt;strong&gt;500 HTTP requests per second&lt;/strong&gt; and often crashed or dropped requests without clear indication of why.. Our main suspect was the custom plugin, based on similar GitHub issues and strange CPU/memory usage reported by its process. We tried scaling Kong from 7 pods up to 15 pods, some of them were still failing...&lt;/p&gt;

&lt;p&gt;Our second issue impacted mostly our Web development team as the auth layer wasn't directly in the code they were working on, nor was it in the same language (Go instead of NodeJS), nor could it support very specific rules per endpoint.&lt;/p&gt;
&lt;h2&gt;
  
  
  Possible solutions ?
&lt;/h2&gt;

&lt;p&gt;Our first and easiest attempt was to optimize options passed to Kong to operate with better performance, as well as review the plugin’s code in depth to fix some memory leaks with database sockets left open. That didn’t help much.&lt;/p&gt;

&lt;p&gt;Then we started looking into Kong alternatives, and found multiple options: &lt;a href="https://gateway.envoyproxy.io/" rel="noopener noreferrer"&gt;Envoy&lt;/a&gt;, &lt;a href="https://traefik.io/traefik" rel="noopener noreferrer"&gt;Traefik&lt;/a&gt;, etc... But migrating from Kong plugin into those would be a bit painful and wasn't guaranteed to be smooth as we use a single domain and it can point to only one of those Ingress controllers.&lt;/p&gt;

&lt;p&gt;Our 3rd option was to use a &lt;em&gt;simple&lt;/em&gt; NestJS middleware that would do the same checks the Kong plugin was doing. This solution would make the auth process more maintainable and flexible, while being directly integrated into the &lt;a href="https://docs.nestjs.com/faq/request-lifecycle#summary" rel="noopener noreferrer"&gt;NestJS request lifecycle&lt;/a&gt;, our internal libs (error messages, logging, caching, database models, etc...). We went with that solution.&lt;/p&gt;
&lt;h2&gt;
  
  
  Progressive migration
&lt;/h2&gt;

&lt;p&gt;As mentioned in the preamble, we have around 15 separate API codebases, dedicated to various business objectives. Copy-pasting the same code in each of them would be a terrible idea to have a maintainable solution over time. &lt;/p&gt;

&lt;p&gt;Luckily, we already had a shared internal library (&lt;code&gt;common-api&lt;/code&gt;) that provides utilities, functions, configurations, and plugins reusable across all services.&lt;br&gt;

  For example, here is our common NestJS boilerplate code:
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;NestFactory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NestFastifyApplication&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;TopLevelModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FastifyAdapter&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ignoreTrailingSlash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Allow all origins for CORS&lt;/span&gt;
        &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DELETE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// Allow these HTTP methods&lt;/span&gt;
        &lt;span class="na"&gt;preflightContinue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Do not pass the preflight response to the next handler&lt;/span&gt;
        &lt;span class="na"&gt;optionsSuccessStatus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Use 204 for successful OPTIONS requests&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;bufferLogs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setGlobalPrefix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalPipes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ValidationPipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;TransformationOptions&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExceptionsFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HttpAdapterHost&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalInterceptors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuroraInterceptor&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// UPDATE: Disabled compression due to RAM usage and CPU load it creates&lt;/span&gt;
  &lt;span class="c1"&gt;// Compression should be registered before static plugin&lt;/span&gt;
  &lt;span class="c1"&gt;// https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f&lt;/span&gt;
  &lt;span class="c1"&gt;// await app.register&amp;lt;FastifyCompressOptions&amp;gt;(compression, {&lt;/span&gt;
  &lt;span class="c1"&gt;//   // Disable `br` encoding as it takes ages to compress whazzup and other huge response payloads&lt;/span&gt;
  &lt;span class="c1"&gt;//   encodings: ['deflate', 'gzip', 'identity'],&lt;/span&gt;
  &lt;span class="c1"&gt;// });&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fastifyStaticPlugin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;root&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;STATIC_FILES_ROOT_PATH&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="c1"&gt;// Backwards compatibility with express for @nestjs/passport https://github.com/nestjs/passport/issues/60&lt;/span&gt;
  &lt;span class="c1"&gt;// Source: https://github.com/nestjs/nest/issues/5702#issuecomment-979893525&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHttpAdapter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getInstance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addHook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;onRequest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
      &lt;span class="nf"&gt;done&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;swaggerOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DocumentBuilder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTitle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IVAO Web Services&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://wiki.ivao.aero/en/home/devops/api/documentation-v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web@ivao.aero&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;useGlobalGuards&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AuthGuard&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Reflector&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

  &lt;span class="nx"&gt;swaggerOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;swaggerOptions&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addBearerAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;openIdConnect&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;openIdConnectUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.ivao.aero/.well-known/openid-configuration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;oauth2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addSecurity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;consumer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apiKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apiKey&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;SwaggerModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDocument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;swaggerOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;SwaggerModule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`docs/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;enableHealthChecks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;uncaughtException&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unhandledRejection&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejectionHandled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

&lt;p&gt;The implementation was pretty straightforward thanks to NestJS framework.&lt;br&gt;
We added 2 new &lt;a href="https://docs.nestjs.com/middleware" rel="noopener noreferrer"&gt;NestJS middlewares&lt;/a&gt;, enabled by default in a non-blocking mode, just adding metadata into the request object passed along:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Key middleware&lt;/strong&gt;: Checks for an API Key passed as Header or Query Param, ensure it's valid, and loads associated application's details.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JWT middleware&lt;/strong&gt;: Checks for a JWT Bearer token present in the &lt;code&gt;Authorization&lt;/code&gt; header, verifies the signature as being issued by IVAO's OAuth service, decodes the content before passing it down the &lt;a href="https://docs.nestjs.com/faq/request-lifecycle#summary" rel="noopener noreferrer"&gt;NestJS request lifecycle&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now that we authenticated the request, we need to authorize it by blocking forbidden requests. For this we have added a series of &lt;a href="https://docs.nestjs.com/guards" rel="noopener noreferrer"&gt;NestJS guards&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;User Authentication&lt;/strong&gt;: Only JWT tokens issued to browser users by our SSO are allowed. &lt;em&gt;Ensures this isn't a bot/application making the request&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Authentication&lt;/strong&gt;: Only API Keys or &lt;a href="https://oauth.net/2/grant-types/client-credentials/" rel="noopener noreferrer"&gt;OAuth2 Client Credentials&lt;/a&gt; authentications are allowed. &lt;em&gt;Usually bots and autonomous applications, or internal tools&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Official applications&lt;/strong&gt;: Only IVAO-issued applications are allowed. &lt;em&gt;Especially for sensitive actions and data that we don't want to grant 3rd-party access to&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required OAuth2 Permission&lt;/strong&gt;: Requires the user to have a specific permission based on their staff position within IVAO. &lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Required OAuth2 Scope&lt;/strong&gt;: Ensures the user has granted consent to the application before making the given request. &lt;em&gt;Such as performing actions on their behalf or retrieve personal data&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to those middlewares being enabled across all our APIs, we can now decorate each endpoint with the guard we want, instead of having a separate config file related to k8s instead of the code itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Did it work ?
&lt;/h2&gt;

&lt;p&gt;Code-wise, the migration was pretty smooth and we were able to quickly decommission our custom plugin, and let each API do the auth checks while having the most up-to-date code. &lt;/p&gt;

&lt;p&gt;However, we did have to increase the resources on some of the API pods due to this additional logic being added on their pods instead of the central Kong deployment. &lt;/p&gt;

&lt;p&gt;Ever since that migration, we didn't experience any issues with kong, which confirmed that moving the auth logic away was the right call.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>systemdesign</category>
      <category>api</category>
      <category>nestjs</category>
    </item>
    <item>
      <title>From TypeScript to SQL: Automatically Granting DB Permissions Without Losing Your Mind</title>
      <dc:creator>David Tchekachev</dc:creator>
      <pubDate>Tue, 12 Aug 2025 08:07:36 +0000</pubDate>
      <link>https://dev.to/ivao/from-typescript-to-sql-automatically-granting-db-permissions-without-losing-your-mind-2am3</link>
      <guid>https://dev.to/ivao/from-typescript-to-sql-automatically-granting-db-permissions-without-losing-your-mind-2am3</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;Managing database grants across multiple microservice APIs became a repetitive, error-prone task for us at IVAO. Developers often forgot to request new permissions, leading to fragile deployments.&lt;/p&gt;

&lt;p&gt;We built a tool extending the Typescript compiler that &lt;strong&gt;analyzes the API source code (using AST traversal)&lt;/strong&gt; to automatically detect which Sequelize models—and therefore which database tables and operations—are actually used. It then &lt;strong&gt;generates the exact SQL &lt;code&gt;GRANT&lt;/code&gt; statements needed&lt;/strong&gt;, with zero manual input.&lt;/p&gt;

&lt;p&gt;The tool is fully integrated into our CI/CD pipeline and Kubernetes setup, eliminating human error and keeping permissions minimal. We open-sourced it here:&lt;/p&gt;

&lt;p&gt;-&amp;gt; &lt;a href="https://github.com/ivaoaero/sequelize-grant-generator/" rel="noopener noreferrer"&gt;ivaoaero/sequelize-grant-generator&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit of context
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Preamble
&lt;/h3&gt;

&lt;p&gt;At &lt;a href="https://ivao.aero" rel="noopener noreferrer"&gt;IVAO&lt;/a&gt;, we partially follow micro-services best practices. This means each business group (e.g., Flight tracking, Flight planning, Air Traffic controlling, Training) has its own &lt;a href="https://nestjs.com/" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt; API and &lt;a href="https://react.dev/" rel="noopener noreferrer"&gt;ReactJS&lt;/a&gt; frontend. But we took a shortcut on the DB side, as we have quite some references across systems, especially for user IDs, airports, etc... We decided to have a single MariaDB instance while having a dedicated logical database for each API, so we still have a &lt;a href="https://en.wikipedia.org/wiki/Separation_of_concerns" rel="noopener noreferrer"&gt;separation of concerns&lt;/a&gt; but we can guarantee data integrity with foreign keys across those databases.&lt;/p&gt;

&lt;h3&gt;
  
  
  The &lt;em&gt;"issue"&lt;/em&gt; we were facing
&lt;/h3&gt;

&lt;p&gt;Although each API has &lt;code&gt;SELECT, INSERT, UPDATE, DELETE&lt;/code&gt; grants on all tables in its own database, we didn’t want to grant these permissions on all databases, as per the &lt;a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege" rel="noopener noreferrer"&gt;principle of least privilege&lt;/a&gt;, we only grant what is actually used by the API.&lt;/p&gt;

&lt;p&gt;Each time a new feature required new grants, the developer would grant it manually in the &lt;code&gt;staging&lt;/code&gt; database to make sure everything worked, and then ask a Tech Lead to do the same in the &lt;code&gt;production&lt;/code&gt; database. Now you start understanding how many issues can occur with this workflow...&lt;/p&gt;

&lt;p&gt;We needed a deterministic way to manage the DB permissions for each of our APIs to prevent human errors and speed-up our deployment time and migration time when we deploy a new DB instance.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;To be honest, this takes us only a few minutes a year, but it's a boring and repetitive task...&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Some unsuccessful attempts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Documenting the needed grants
&lt;/h3&gt;

&lt;p&gt;Our first idea was to list all needed grants in the &lt;code&gt;README.md&lt;/code&gt; file of the service, so any changes would be easily visible in a Pull Request, hoping the Tech Lead would notice it and make the changes accordingly.&lt;/p&gt;

&lt;p&gt;But we anticipated that developers might forget about updating that file and it would quickly end up out-of-sync... Also, it's not a very &lt;em&gt;clean&lt;/em&gt; solution. So we decided against it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using a 3rd-party tool
&lt;/h3&gt;

&lt;p&gt;On the open-source community, there are many tools that allow you to configure your MariaDB instance with configuration files, as per &lt;a href="https://en.wikipedia.org/wiki/Infrastructure_as_code" rel="noopener noreferrer"&gt;Infrastructure as Code&lt;/a&gt; principle.&lt;/p&gt;

&lt;p&gt;With such solution, the developer would need to update a config file (located in the same repository or not) with the needed grants and it would be picked up by the tool that updates the grants automatically.&lt;/p&gt;

&lt;p&gt;Although it looked like an elegant solution, it still required developers to manually edit an additional file after working on a feature in the code, and could be forgotten since there was nothing checking its integrity.&lt;/p&gt;

&lt;h3&gt;
  
  
  A True Source of Truth, Without Extra Work?
&lt;/h3&gt;

&lt;p&gt;Both solutions above forced developers to review the code they worked on, identify the new grants to add or remove, and finally update a separate file to reflect those changes.&lt;/p&gt;

&lt;p&gt;I was convinced we could find a way to manage those grants automatically so the developer didn't have to think about it and just import the models he needed in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Over-engineering It (Because Why Not?)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfp1nt69ewpdps5nx71s.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzfp1nt69ewpdps5nx71s.jpg" alt="spend 10 minutes doing the task manually vs spend 10 hours writing code to automate it drake" width="735" height="735"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://starecat.com/spend-10-minutes-doing-the-task-manually-vs-spend-10-hours-writing-code-to-automate-it-drake/" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first question I asked myself: &lt;em&gt;"How can a program know which grants an API needs ?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The answer was: &lt;em&gt;"Go look in the API's code and figure it out !"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Just as a reminder, our APIs are coded in Typescript with &lt;a href="https://nestjs.com/" rel="noopener noreferrer"&gt;NestJS&lt;/a&gt; and &lt;a href="https://sequelize.org/" rel="noopener noreferrer"&gt;Sequelize&lt;/a&gt; as our &lt;a href="https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping" rel="noopener noreferrer"&gt;ORM&lt;/a&gt; (with the &lt;a href="https://github.com/sequelize/sequelize-typescript" rel="noopener noreferrer"&gt;Typescript&lt;/a&gt; extension).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;My first thought was to write Regexes to extract the models and their usage from the source code, but I knew it would turn into a messy rabbit hole even before I begin.&lt;/p&gt;

&lt;p&gt;At this moment I had a flashback from a recent Computer Science course I had: &lt;a href="https://en.wikipedia.org/wiki/Compiler" rel="noopener noreferrer"&gt;Program Compilers&lt;/a&gt; in which we studied the theory behind them and built our own compiler for a simple language (Tiger). There I learnt that compilers actually analyze the whole code and build an &lt;a href="https://en.wikipedia.org/wiki/Abstract_syntax_tree" rel="noopener noreferrer"&gt;Abstract Syntax Tree&lt;/a&gt; (AST) that represents the instructions with the correct types, binding, checks, etc...&lt;/p&gt;

&lt;p&gt;Indeed, Typescript knows exactly which classes and variables were Sequelize models (as each model extends a shared base &lt;a href="https://github.com/sequelize/sequelize-typescript/blob/master/src/model/model/model.ts" rel="noopener noreferrer"&gt;&lt;code&gt;Model&lt;/code&gt;&lt;/a&gt; class) and what methods could be used on them!&lt;/p&gt;

&lt;p&gt;One of the key moments from the course that stayed with me, was a quote from the professor: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can build compiler plugins for any compiled languages to override/extend some actions&lt;/p&gt;

&lt;p&gt;-- &lt;cite&gt;&lt;a href="https://www.epita.fr/en/" rel="noopener noreferrer"&gt;EPITA&lt;/a&gt; professors: Étienne Renaut, Ghiles Ziat, Loïc Blet&lt;/cite&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Although Typescript isn't a &lt;em&gt;compiled&lt;/em&gt; language, it still type-checks the code before transpiling it to JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leverage Typescript type-checking to identify used DB models
&lt;/h3&gt;

&lt;p&gt;In our case, all DB models are defined in a separate NPM package (&lt;code&gt;@ivao.aero/database&lt;/code&gt;), which we import in all our APIs, so models can be referenced from other APIs if needed (&lt;em&gt;as explained in the preamble&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;In 95% of the case, here is how the code looks like in a given service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Airport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Country&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ivaoaero/database&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CountryService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;getCountriesWithAirports&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findAll&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="cm"&gt;/* args */&lt;/span&gt;
            &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Airport&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;// Do some processing&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that code, I need my API to have &lt;code&gt;SELECT&lt;/code&gt; grants on &lt;code&gt;airports&lt;/code&gt;, &lt;code&gt;airport_countries&lt;/code&gt; (hidden Many-to-Many table), and &lt;code&gt;countries&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;By breaking down the problem at hand, I realized I could make it easier by identifying the classes imported at the top of the file, as they would always come from the same package. This is when I found a &lt;a href="https://stackoverflow.com/questions/75650470/how-can-i-get-a-list-of-all-individual-imports-in-a-ts-file-with-their-absolute/78130948#78130948" rel="noopener noreferrer"&gt;StackOverflow answer&lt;/a&gt; that helped me a lot to lay the foundations for this crazy project.&lt;/p&gt;

&lt;p&gt;I also realized that working with AST is very portable from one language to the other, once you understand the theory behind it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Iterating to improve the tool
&lt;/h3&gt;

&lt;p&gt;At my first attempt, my tool was able to load a Typescript project from the &lt;code&gt;tsconfig.json&lt;/code&gt; file, go through each file to get the AST built by the Typescript compiler, and extract all the models imported in the project (&lt;code&gt;ImportDeclaration&lt;/code&gt; nodes).&lt;br&gt;
But I still had 2 missing issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I didn't know how those models were used. Did the code only require &lt;code&gt;SELECT&lt;/code&gt; or needed &lt;code&gt;INSERT&lt;/code&gt;, &lt;code&gt;UPDATE&lt;/code&gt;, &lt;code&gt;DELETE&lt;/code&gt; as well ?&lt;/li&gt;
&lt;li&gt;I was missing the hidden Many-to-Many tables that are used when building nested relations, as they aren't directly referenced in the code (whole point of the ORM is to abstract this ;))&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I understood that I needed to traverse the whole AST, instead of stopping at the import statements, to find the models usage (&lt;code&gt;CallExpression&lt;/code&gt; nodes).&lt;/p&gt;

&lt;p&gt;It wasn't as straightforward as processing the imports because you can have some edge-cases like this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Airport&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@ivaoaero/database&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AirportEntity&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;PickType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Airport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AirportEntity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;Airport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findOne&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* args */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* args */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, &lt;code&gt;instance&lt;/code&gt; isn't of type &lt;code&gt;Airport&lt;/code&gt; but &lt;code&gt;AirportEntity&lt;/code&gt; as I have selected only from fields/attributes. My tool wasn't able to understand that &lt;code&gt;.update&lt;/code&gt; actually referenced the Sequelize method from the parent class.&lt;/p&gt;

&lt;p&gt;I will not go into details on every blocker I encountered but I can confirm it was quite &lt;em&gt;fun&lt;/em&gt; to debug Typescript typings to figure out which ones were actual Sequelize models. Also, finding hidden Many-to-Many relations between used models...&lt;/p&gt;

&lt;p&gt;After some long evenings, I finally had a working tool !&lt;/p&gt;

&lt;h3&gt;
  
  
  How does it work ?
&lt;/h3&gt;

&lt;p&gt;Now that the tool is finished, here is how it works: &lt;strong&gt;Give it the folder in which the API is stored, and it will return a SQL query&lt;/strong&gt; which will grant that API's DB user all the grants it needs to function!&lt;/p&gt;

&lt;p&gt;For IVAO, we have integrated this tool into our CI/CD pipeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When we run &lt;code&gt;yarn build&lt;/code&gt; in our pipelines, it also exports the needed grants into a file stored in &lt;code&gt;dist/&lt;/code&gt; which is packaged in the Docker image&lt;/li&gt;
&lt;li&gt;In our K8s environment, we added an &lt;a href="https://kubernetes.io/docs/concepts/workloads/pods/init-containers/" rel="noopener noreferrer"&gt;Init Container&lt;/a&gt; that starts just before the API, reads the needed grants from the packaged file, and applies them.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  To conclude
&lt;/h2&gt;

&lt;p&gt;With this tool fully integrated into our development process, developers no longer need to think about DB grants—whether adding new ones or cleaning up unused permissions. The codebase itself is now the &lt;strong&gt;source of truth&lt;/strong&gt; for what’s needed at runtime.&lt;/p&gt;

&lt;p&gt;We’ve open-sourced it for others who might face similar issues: &lt;a href="https://github.com/ivaoaero/sequelize-grant-generator/" rel="noopener noreferrer"&gt;GitHub sequelize-grant-generator&lt;/a&gt;. There, the complete parsing process is documented if you want more details.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Sure, the manual process wasn’t a huge burden. But building this tool? That was the fun part.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>typescript</category>
      <category>sequelize</category>
    </item>
    <item>
      <title>We Trust Cargo — But libmysqlclient Taught Us to Check the System Too</title>
      <dc:creator>David Tchekachev</dc:creator>
      <pubDate>Tue, 29 Jul 2025 08:00:00 +0000</pubDate>
      <link>https://dev.to/ivao/we-trust-cargo-but-libmysqlclient-taught-us-to-check-the-system-too-1190</link>
      <guid>https://dev.to/ivao/we-trust-cargo-but-libmysqlclient-taught-us-to-check-the-system-too-1190</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;After bumping some dependencies in our Rust Actix API, we experienced a very strange deserialization issues from our ORM (diesel) on nullable values in an endpoint that used to work perfectly.&lt;/p&gt;

&lt;p&gt;Turns out it was due to a version of &lt;code&gt;libmysqlclient&lt;/code&gt; installed at OS-level in our containers that wasn't returning the right value in specific cases. It took us a couple of days to figure it out.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit of context
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Preamble
&lt;/h3&gt;

&lt;p&gt;At &lt;a href="https://ivao.aero" rel="noopener noreferrer"&gt;IVAO&lt;/a&gt;, we need to serve some APIs for a large amount of users: &lt;strong&gt;30M requests / day&lt;/strong&gt; (avg. &lt;strong&gt;330 rps&lt;/strong&gt;) with short data liveness: &lt;strong&gt;10 seconds&lt;/strong&gt; (with plans to reduce it even more).&lt;/p&gt;

&lt;p&gt;Of course, we have some caching in place to reduce the load on our APIs and database. For more details, please refer to &lt;a href="https://virtualsky.ivao.aero/next-steps-for-2025/#%F0%9F%A7%A0-caching-tracker-data" rel="noopener noreferrer"&gt;the post we wrote on our blog&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you are interested in understanding why we have some APIs in NestJS and others in Rust, please refer to our previous article&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f" class="crayons-story__hidden-navigation-link"&gt;Why We Had to Switch from NestJS to Rust Over One Hidden (and Costly) Setting&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;
          &lt;a class="crayons-logo crayons-logo--l" href="/ivao"&gt;
            &lt;img alt="IVAO logo" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F9651%2F4a677d52-0c21-4d3b-bda3-4b65e679f0fc.png" class="crayons-logo__image"&gt;
          &lt;/a&gt;

          &lt;a href="/tchekda" class="crayons-avatar  crayons-avatar--s absolute -right-2 -bottom-2 border-solid border-2 border-base-inverted  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3346854%2F381749d3-afeb-49a6-97d6-b7efd287eb5b.jpg" alt="tchekda profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/tchekda" class="crayons-story__secondary fw-medium m:hidden"&gt;
              David Tchekachev
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                David Tchekachev
                
              
              &lt;div id="story-author-preview-content-2709038" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/tchekda" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3346854%2F381749d3-afeb-49a6-97d6-b7efd287eb5b.jpg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;David Tchekachev&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

            &lt;span&gt;
              &lt;span class="crayons-story__tertiary fw-normal"&gt; for &lt;/span&gt;&lt;a href="/ivao" class="crayons-story__secondary fw-medium"&gt;IVAO&lt;/a&gt;
            &lt;/span&gt;
          &lt;/div&gt;
          &lt;a href="https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Jul 20 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f" id="article-link-2709038"&gt;
          Why We Had to Switch from NestJS to Rust Over One Hidden (and Costly) Setting
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/nestjs"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;nestjs&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/rust"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;rust&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/webdev"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;webdev&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/api"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;api&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
            &lt;a href="https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            5 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


&lt;h3&gt;
  
  
  Some unrelated migration
&lt;/h3&gt;

&lt;p&gt;We were in the process of migrating how our APIs authenticated and authorized our users &amp;amp; applications. Without going into too many details, the JWT validation used to be done by our proxy (&lt;a href="https://konghq.com/" rel="noopener noreferrer"&gt;Kong&lt;/a&gt;) and was being migrated to the end-APIs.&lt;/p&gt;

&lt;p&gt;To simplify, the initial plan was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a JWT middleware to our &lt;a href="https://actix.rs/" rel="noopener noreferrer"&gt;Actix&lt;/a&gt; API &lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wanting to make it quick &amp;amp; easy, ended up in a rabbit-hole
&lt;/h2&gt;

&lt;p&gt;During the above migration, we needed to import new crates into our project: &lt;code&gt;actix-web-httpauth, jwt-simple-jwks, jwt-simple&lt;/code&gt;, nothing too crazy considering what we were doing.&lt;/p&gt;

&lt;p&gt;But, here is where it started to get &lt;em&gt;slightly&lt;/em&gt; interesting: those new crates have dependencies (&lt;em&gt;in particular &lt;code&gt;base64&lt;/code&gt;&lt;/em&gt;) that require &lt;u&gt;Rust Edition 2024&lt;/u&gt;, while we used to be on Rust Edition 2021, version 1.79.&lt;/p&gt;

&lt;p&gt;At this point, our thought was: &lt;em&gt;"Let's bump the edition, rust, and take the opportunity to run &lt;code&gt;cargo update&lt;/code&gt;"&lt;/em&gt;. Shouldn't be too big of a deal, right? At least we thought it would be easier than finding a &lt;em&gt;non-latest&lt;/em&gt; version of the crates that didn't require the 2024 edition.&lt;/p&gt;

&lt;p&gt;Here are the steps we ended up taking:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We upgraded from Rust 1.79 to Rust 1.87 (latest version available at the time), both in our local environment and in our Docker containers building &amp;amp; running the API.

&lt;ul&gt;
&lt;li&gt;As Rust 1.79 didn't support the 2024 edition.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;We upgraded from &lt;code&gt;edition: 2021&lt;/code&gt; to &lt;code&gt;edition: 2024&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;We ran &lt;code&gt;cargo update&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;We installed the new crates: &lt;code&gt;actix-web-httpauth, jwt-simple-jwks, jwt-simple&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  It was too easy to be true
&lt;/h3&gt;

&lt;p&gt;At this point, &lt;u&gt;in the local environment&lt;/u&gt;, everything worked great and we were able to add the auth middleware we wanted and &lt;strong&gt;successfully ran all tests&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;We, then, deployed the API to our staging environment to ensuring it integrated well with the services querying it. Although we already tested it locally, nothing beats integration tests in a dedicated environment.&lt;/p&gt;

&lt;p&gt;And here the real issue was noticed: We were getting 500's on some of our endpoints!&lt;/p&gt;

&lt;h3&gt;
  
  
  Starting to dig into the 500's Server Error
&lt;/h3&gt;

&lt;p&gt;When we noticed the 500 response code, we went to our logs, only to find the following error message there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; DeserializationError(DeserializeFieldError { field_name: Some("arrival_distance"), error: TryFromSlideError(()) })
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which comes from our ORM, &lt;a href="https://diesel.rs/" rel="noopener noreferrer"&gt;diesel&lt;/a&gt;, that we haven't directly touched during the migration.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: &lt;code&gt;arrival_distance&lt;/code&gt; is nullable LONG which holds the miles left to destination for a given plane from a reported GPS position. It is returned in the endpoints failing but not used in the middleware at any point.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;At first we thought something strange was happening in our staging database and it might be corrupted, explaining the deserialization error. But we didn't find anything there, and running the API locally while being connected to the same database didn't throw any errors.&lt;/p&gt;

&lt;p&gt;Looking on Google, nothing was returned for that error message, except the code throwing it ;)&lt;/p&gt;

&lt;p&gt;Since the issue was only happening in our staging environment, our first goal was to replicate it locally, which would greatly improve our velocity in solving it, seeing it couldn't be solved quickly.&lt;/p&gt;

&lt;p&gt;As much as I tried, I was not able to replicate the issue locally, I checked the rust version, the dependencies, I reinstalled everything, nothing worked...&lt;/p&gt;

&lt;p&gt;After some time with unconclusive results, I called for some backup with another developer who had greatly contributed to the project in the past. To my surprise, he was able to replicate the issue on his computer.&lt;/p&gt;

&lt;p&gt;At this point, it was the exact representation of &lt;strong&gt;It works on my machine&lt;/strong&gt;, although the changes in the PR didn't contain anything indicating device-specific environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojebxa7ukfkrix6ctg43.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fojebxa7ukfkrix6ctg43.png" alt="Works on my machine"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We tried reverting &lt;u&gt;some&lt;/u&gt; of the changes we did, like uninstalling the new crates (which had nothing to do the deserialization), reverting the edition upgrade, and even downgrading Rust back to 1.79.&lt;br&gt;
But the error was still happening on staging environment but still not reproducible on my computer...&lt;/p&gt;

&lt;p&gt;Until then, I was running the API directly from my Linux machine (&lt;a href="https://nixos.org/" rel="noopener noreferrer"&gt;NixOS&lt;/a&gt;) but our staging environment is running the APIs in Docker containers.&lt;br&gt;

  Our Dockerfile
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;rust:1.87-slim-bullseye&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /build&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; pkg-config libssl-dev default-libmysqlclient-dev

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; Cargo.toml Cargo.lock ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; crates/ crates/&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/build/target &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/cargo/registry &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/cargo/git &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/rustup &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-eux&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    rustup &lt;span class="nb"&gt;install &lt;/span&gt;1.87.0&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    cargo &lt;span class="nb"&gt;fmt&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\ &lt;/span&gt;
    cargo build --release; \
    objcopy --compress-debug-sections target/release/tracker-api ./tracker-api

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; docker.io/debian:bullseye-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; pkg-config libssl-dev default-libmysqlclient-dev

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 8000&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /build/tracker-api ./tracker-api&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ./tracker-api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We know the Dockerfile could be improved—but that's a topic for another day 😉_&lt;br&gt;
&lt;/p&gt;

&lt;br&gt;
&lt;br&gt;&lt;br&gt;
I decided to try to run the container locally although it wasn't that easy as connecting to services outside the docker network is a bit tricky (connection to staging services to test locally was done with &lt;code&gt;kubectl port-forward&lt;/code&gt; which isn't accessible from Docker's network).&lt;/p&gt;

&lt;p&gt;Finally, after overcoming the network hurdles, I was able to run the API in a docker containers on my PC, and the error was finally happening on my PC as well! First win!&lt;/p&gt;

&lt;h3&gt;
  
  
  Trying to find a solution
&lt;/h3&gt;

&lt;p&gt;Now the question was: &lt;strong&gt;Why is the API working on my NixOS, but not in Docker ?&lt;/strong&gt; &lt;br&gt;
Also, &lt;strong&gt;How is it related to deserialization ?&lt;/strong&gt; as we haven't touched that part.&lt;/p&gt;

&lt;p&gt;While searching through the deep ends of the internet, I found people trying to bundle the &lt;a href="https://crates.io/crates/mysqlclient-sys" rel="noopener noreferrer"&gt;&lt;code&gt;mysqlclient-sys&lt;/code&gt;&lt;/a&gt; crate directly into the target binary, instead of relying on dynamic linking to the OS-installed version (&lt;em&gt;see Dockerfile above&lt;/em&gt;). We tried to do the same thing, but this required quite some additional build tools and still wasn't compiling, so we abandoned that path.&lt;/p&gt;

&lt;p&gt;But we kept pursuing that direction, thinking it was indeed related to the MySQL client library. This is when we started looking more closely at the &lt;code&gt;default-libmysqlclient-dev&lt;/code&gt; installed in the Docker container, and the one on my NixOS. We realized that &lt;u&gt;we had different versions&lt;/u&gt; of that library!&lt;/p&gt;

&lt;p&gt;By checking the Debian registry, we confirmed that our Docker container wasn't using the latest version (&lt;a href="https://packages.debian.org/bullseye/default-libmysqlclient-dev" rel="noopener noreferrer"&gt;Debian 11 package version&lt;/a&gt; vs &lt;a href="https://packages.debian.org/bookworm/default-libmysqlclient-dev" rel="noopener noreferrer"&gt;Debian 12 package version&lt;/a&gt;) while my NixOS was. And we also realized that we hadn't reverted the &lt;code&gt;cargo update&lt;/code&gt; we performed during the initial migration, which actually bumped &lt;code&gt;diesel&lt;/code&gt; to a new version (&lt;code&gt;2.1.6&lt;/code&gt; -&amp;gt; &lt;code&gt;2.2.11&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;When rebuilding our Docker image with &lt;u&gt;Debian 12 instead of Debian 11&lt;/u&gt; ( &lt;code&gt;:bullseye-slim&lt;/code&gt; --&amp;gt; &lt;code&gt;:bookworm-slim&lt;/code&gt;), &lt;strong&gt;the error was finally gone!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  To conclude
&lt;/h2&gt;

&lt;p&gt;From our perspective, it seems like &lt;code&gt;libmysqlclient&lt;/code&gt; and/or &lt;code&gt;diesel&lt;/code&gt; have made some changes that made some &lt;em&gt;non-latest&lt;/em&gt; version incompatible.&lt;br&gt;
Using the latest versions everywhere has fixed the issue.&lt;/p&gt;

&lt;p&gt;Although Rust has a package manager (Cargo) that ensures crates are linked to other crates with specific versions, this doesn't concern external libs linked by the OS (&lt;code&gt;libmysqlclient&lt;/code&gt; in our case), no version checks are happening to ensure compatibility.&lt;/p&gt;

&lt;p&gt;Side Note: Only a few days after discovering this bug, haven't had the time to properly report it, other users have already &lt;a href="https://github.com/diesel-rs/diesel/discussions/4630" rel="noopener noreferrer"&gt;reported it on GitHub&lt;/a&gt; and a &lt;a href="https://github.com/diesel-rs/diesel/commit/0c02a66c49a18db787ff4713d030c77f9b20ec89" rel="noopener noreferrer"&gt;fix was submitted&lt;/a&gt; quickly afterwards. If we had performed that migration after the bug was discovered and fixed, we would have been able to avoid it or fix it faster.&lt;/p&gt;

&lt;p&gt;Side Note²: Looking at the &lt;a href="https://github.com/diesel-rs/diesel/commit/0c02a66c49a18db787ff4713d030c77f9b20ec89" rel="noopener noreferrer"&gt;fix applied&lt;/a&gt;, it indeed matches our specific case (&lt;code&gt;nullable LONG value&lt;/code&gt;). But we wouldn't have been able to pinpoint it as we didn't dig that deep.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>sql</category>
      <category>debian</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why We Had to Switch from NestJS to Rust Over One Hidden (and Costly) Setting</title>
      <dc:creator>David Tchekachev</dc:creator>
      <pubDate>Sun, 20 Jul 2025 23:08:27 +0000</pubDate>
      <link>https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f</link>
      <guid>https://dev.to/ivao/why-we-switched-from-nestjs-to-rust-over-one-hidden-and-costly-setting-d9f</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;We partially migrated to Rust because NestJS's compression middleware had a default configuration at max level, which noticeably impacted our performance - something that took months to identify and fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  A bit of context
&lt;/h2&gt;

&lt;p&gt;As presented in our &lt;a href="https://dev.to/ivao/how-we-handle-megabytes-of-real-time-data-in-react-with-indexeddb-1760"&gt;previous post&lt;/a&gt;, IVAO has a website displaying live data traffic to thousands of visitors at the same time: &lt;a href="https://webeye.ivao.aero" rel="noopener noreferrer"&gt;Webeye&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The website operates fairly simply, it fetches the data every 15 seconds from 3 different endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/now/pilots&lt;/code&gt;: 3 MB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/now/atcs&lt;/code&gt;: 3 MB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/now/observers&lt;/code&gt;: 1 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also have specific endpoints which are fetched when you click on a specific user:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/sessions/:id&lt;/code&gt;: 0.5 MB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/sessions/:id/tracks&lt;/code&gt;: 0-5 MB&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/whazzup&lt;/code&gt;: 5 MB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each endpoint used to take around &lt;strong&gt;2 seconds&lt;/strong&gt; to reply, which was &lt;strong&gt;unacceptably slow&lt;/strong&gt; !! We understand NestJS (based on fastify) isn't the go-to framework for low-latency APIs, but it's the easiest to maintain across the 20+ APIs we have, and it's easy to find volunteers with the skills to do so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Let's try to find the culprit !
&lt;/h2&gt;

&lt;p&gt;First, we checked the NestJS best-practices for performance and confirmed that we were already using &lt;a href="https://docs.nestjs.com/techniques/performance" rel="noopener noreferrer"&gt;fastify&lt;/a&gt; instead of &lt;code&gt;express&lt;/code&gt;. And we already had &lt;a href="https://docs.nestjs.com/techniques/compression" rel="noopener noreferrer"&gt;compression&lt;/a&gt; enabled to reduce the response payload size.&lt;/p&gt;

&lt;p&gt;Also, at that time (which isn't the case when I'm writing this article), the authentication &amp;amp; authorization part was handled by &lt;a href="https://konghq.com/" rel="noopener noreferrer"&gt;Kong&lt;/a&gt;, our Kubernetes proxy, so the API would only have to serve the data and not waste compute time on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;

&lt;p&gt;Most of the time, when an API is slow, the first attempt to speed it up is to cache it.&lt;/p&gt;

&lt;p&gt;We have tried to implement caching at CloudFlare level, but it didn't work as all our endpoints are authenticated with an API Key or Bearer token, and Cloudflare doesn't cache those resources (which makes sense).&lt;/p&gt;

&lt;p&gt;We have also tried implementing Redis caching which seemed to work well but the first person to make the request would still have to wait those long seconds. While it’s technically possible to pre-populate the cache with a background task, while the API would only be serving it, we decided it would be an ugly and overcomplicated trick that would create new issues. &lt;/p&gt;

&lt;h3&gt;
  
  
  Digging into the code
&lt;/h3&gt;

&lt;p&gt;While debugging, we leveraged JavaScript’s &lt;a href="https://developer.mozilla.org/fr/docs/Web/API/console/time_static" rel="noopener noreferrer"&gt;console.time&lt;/a&gt; feature to pinpoint the function taking so much time. Our conclusion was that the request was received and went through all the middlewares pretty quickly, the database query was already pretty fast, but the response was taking most of that time. &lt;/p&gt;

&lt;p&gt;There, our first guess that it was related to the serialization of our response which came from &lt;a href="https://sequelize.org/" rel="noopener noreferrer"&gt;Sequelize&lt;/a&gt; into JSON.&lt;/p&gt;

&lt;p&gt;We have tried to implement &lt;a href="https://nestia.io/" rel="noopener noreferrer"&gt;Nestia&lt;/a&gt;, which is a NestJS variant that should have &lt;u&gt;200x faster serialization&lt;/u&gt;. After some very long nights fighting with it without any improvement, we came to the conclusion that we were either doing something completely wrong, or the issue wasn't in the serialization process...&lt;/p&gt;

&lt;h3&gt;
  
  
  Out of ideas
&lt;/h3&gt;

&lt;p&gt;At some point, we ran out of ideas, after trying to optimize our NestJS API as much as possible.&lt;br&gt;
As some developers suggested: &lt;em&gt;"Let's try the most state-of-the-art solution for low-latency APIs !"&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Migration to Rust (Actix)
&lt;/h2&gt;

&lt;p&gt;Although I wasn't a big fan of introducing a new language into our tech stack (and still am), we decided to give Actix, a Rust framework for APIs, a go; just to see what we could achieve and if the issue was, indeed, coming from NodeJS.&lt;/p&gt;

&lt;p&gt;Although we struggled a bit — none of us had experience with the framework, we finally replicated the exact same response bodies as in NestJS. The results were better than what we expected: Endpoints had an average of &lt;strong&gt;150ms&lt;/strong&gt; ! &lt;em&gt;(From 2s -&amp;gt; 150ms: 12x faster)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bonus: the Rust API consumed only 50M of RAM, while NestJS consumed over 500M during peak time !! (90% lower memory usage)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There wasn't much to debate, we deployed that Rust API to replace the NestJS one, only for the endpoints frequently accessed by Webeye.&lt;/p&gt;

&lt;p&gt;The user experience greatly improved, while our codebase was split across 2 completely different languages. That’s a debate for another day ;)&lt;/p&gt;
&lt;h1&gt;
  
  
  Being stubborn about keeping NestJS API
&lt;/h1&gt;

&lt;p&gt;Although the Rust API was working very well, it still stayed in mind: &lt;em&gt;"How can NestJS be so slow, and still so widely used"&lt;/em&gt;, so I kept digging!&lt;/p&gt;

&lt;p&gt;During a long night of trying to find a solution, I started noticing something strange: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests made from the browser: 2 seconds&lt;/li&gt;
&lt;li&gt;Requests made from Postman: 2 seconds&lt;/li&gt;
&lt;li&gt;Requests made from curl: &lt;strong&gt;200ms&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;How is it possible that I wasn't getting the same results from curl ?&lt;/u&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Let's try to find the difference between Postman and curl !
&lt;/h3&gt;

&lt;p&gt;I spent a few hours trying to figure out why Postman was so slow at processing requests, only to find that it wasn't Postman's fault....&lt;/p&gt;

&lt;p&gt;Postman, by default, sends headers with the request, including &lt;a href="https://developer.mozilla.org/docs/Web/HTTP/Reference/Headers/Accept-Encoding" rel="noopener noreferrer"&gt;Accept-Encoding&lt;/a&gt;: &lt;code&gt;Accept-Encoding: br, deflate, gzip, *&lt;/code&gt;&lt;br&gt;
Which means that the API will try, in order and if it supports it, to compress the response payload with Brotli, Deflate, Gzip, etc...&lt;/p&gt;

&lt;p&gt;When disabling that default header, Postman also started making &lt;u&gt;200ms requests&lt;/u&gt;!&lt;br&gt;
&lt;strong&gt;We finally found the culprit!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After a bit of digging on the internet, I found the following GitHub issue on the NestJS repo: &lt;a href="https://github.com/nestjs/docs.nestjs.com/issues/2834" rel="noopener noreferrer"&gt;perf: brotli compression&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Basically, it said that Brotli was configured by default on the max setting, which took time to compress the whole payload&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Although the Github Issue recommends setting a lower value to speed up the process, we decided to completely disable Brotli in our APIs as any value we tried to pass would always take longer than the other encodings. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Previous NestJs Bootstrap code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;register&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FastifyCompressOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;New NestJs Bootstrap code:&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;register&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;FastifyCompressOptions&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compression&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Disable `br` encoding&lt;/span&gt;
  &lt;span class="na"&gt;encodings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deflate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gzip&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One might ask: &lt;em&gt;"Aren't you trying to save bandwidth to reduce egress fees?"&lt;/em&gt;, to which we happily reply that we are hosted at &lt;a href="https://www.ovhcloud.com/" rel="noopener noreferrer"&gt;OVH&lt;/a&gt; in France, and there are &lt;strong&gt;0 egress fees&lt;/strong&gt; !&lt;/p&gt;

&lt;p&gt;With this fix in place, our NestJS APIs are now &lt;strong&gt;10x faster&lt;/strong&gt; &lt;em&gt;(2s -&amp;gt; 200ms)&lt;/em&gt; on big payloads, which is way better than before. With those performances, switching to Rust wouldn't have been worth it &lt;em&gt;(except for the RAM usage - but that's not today's problem)&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  To conclude
&lt;/h2&gt;

&lt;p&gt;While trying to reduce the response time of our APIs, we enabled NestJS compression middleware, which turned out to be configured at the maximum setting and was the reason our endpoints were taking so long!&lt;/p&gt;

</description>
      <category>nestjs</category>
      <category>rust</category>
      <category>webdev</category>
      <category>api</category>
    </item>
    <item>
      <title>How We Handle Megabytes of Real-Time Data in React with IndexedDB</title>
      <dc:creator>David Tchekachev</dc:creator>
      <pubDate>Mon, 14 Jul 2025 13:28:43 +0000</pubDate>
      <link>https://dev.to/ivao/how-we-handle-megabytes-of-real-time-data-in-react-with-indexeddb-1760</link>
      <guid>https://dev.to/ivao/how-we-handle-megabytes-of-real-time-data-in-react-with-indexeddb-1760</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;We refactored IVAO's live flight map (Webeye) to reduce memory usage and improve performance. By storing &amp;amp; querying fetched data in IndexedDB, and using event-driven rendering, we:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cut down re-renders by 80%&lt;/li&gt;
&lt;li&gt;Improved browser memory footprint&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  One of our daily struggles
&lt;/h2&gt;

&lt;p&gt;At &lt;a href="https://ivao.aero" rel="noopener noreferrer"&gt;IVAO&lt;/a&gt;, we deliver an aviation simulation network for enthusiasts, our motto being &lt;em&gt;As real as it gets&lt;/em&gt; !&lt;/p&gt;

&lt;p&gt;If you have an aviation fan in your close circle, or if you are one, you know one thing for sure, they can't live without &lt;a href="https://www.flightradar24.com/" rel="noopener noreferrer"&gt;FlightRadar24&lt;/a&gt; (&lt;em&gt;It's a live map of all planes showing where they are going, the type of airplane, etc...)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Logically, we also have our own version: &lt;a href="https://webeye.ivao.aero" rel="noopener noreferrer"&gt;Webeye&lt;/a&gt;, which displays the same information about live flights, online Air Traffic Controllers (ATC), as well as additional data used to better experience the network (e.g., community events, specific sectors). &lt;br&gt;
Unsurprisingly, it’s our most visited website across all the ones we have, we average 50,000 visits per day while rendering approximately &lt;strong&gt;20MB&lt;/strong&gt; of live data.&lt;/p&gt;

&lt;p&gt;Now, the technical challenge is the following: &lt;strong&gt;How to display all that information, live, to all the users ?&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  How it used to be
&lt;/h3&gt;

&lt;p&gt;Webeye is a React frontend, fetching &lt;a href="https://api.ivao.aero" rel="noopener noreferrer"&gt;IVAO REST APIs&lt;/a&gt;, and leveraging &lt;a href="https://openlayers.org/" rel="noopener noreferrer"&gt;OpenLayers&lt;/a&gt; library to render the map with all the overlays (e.g., shapes, planes, clicks, moves, zoom)&lt;/p&gt;

&lt;p&gt;The code being a bit &lt;em&gt;old&lt;/em&gt; (coded in 2020), it followed the basics of React at the time, without too much complexity, which makes it actually easy to maintain for anyone in the team with some knowledge of React.&lt;br&gt;
In that code, all the data was being fetched from their respective endpoints every 15 seconds (&lt;em&gt;we will cover how we manage that in a separate article&lt;/em&gt;), and then stored in global React states because they were referenced in various places (e.g., map rendering, hover popup, details panel, track calculation).&lt;br&gt;
This caused issues like multiple map re-renders when multiple endpoints were refreshed.&lt;/p&gt;

&lt;p&gt;That approach actually worked but was very memory intensive (thank you Chrome profiler for pinpointing it), which made the experience laggy...&lt;/p&gt;
&lt;h2&gt;
  
  
  Delegating data storage and querying to IndexedDB
&lt;/h2&gt;

&lt;p&gt;We started looking into how we could improve the situation and came to the conclusion we needed to reduce the amount of data stored in JavaScript variables. &lt;/p&gt;

&lt;p&gt;Major options available in a browser are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local Storage / Session Storage&lt;/strong&gt;: Key-Value string storage

&lt;ul&gt;
&lt;li&gt;Works great for simple config values or tokens&lt;/li&gt;
&lt;li&gt;Hard to query&lt;/li&gt;
&lt;li&gt;Data needs to be manually (de)serialized each time&lt;/li&gt;
&lt;li&gt;Storage is limited to 10MB&lt;/li&gt;
&lt;li&gt;Synchronous API&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cookie&lt;/strong&gt;: Key-Value string storage

&lt;ul&gt;
&lt;li&gt;Sent to the server on each request (not needed for us)&lt;/li&gt;
&lt;li&gt;Storage is limited to 4KB&lt;/li&gt;
&lt;li&gt;Synchronous API&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;&lt;strong&gt;IndexedDB&lt;/strong&gt;: Objects or key-value pairs&lt;/u&gt;

&lt;ul&gt;
&lt;li&gt;Asynchronous API&lt;/li&gt;
&lt;li&gt;No hard storage limit&lt;/li&gt;
&lt;li&gt;Made for large datasets&lt;/li&gt;
&lt;li&gt;Allows selecting a specific object in a dataset with some filters&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://medium.com/@lancelyao/browser-storage-local-storage-session-storage-cookie-indexeddb-and-websql-be6721ebe32a" rel="noopener noreferrer"&gt;&lt;em&gt;Reference Article about browser storages&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We decided to implement &lt;strong&gt;IndexedDB&lt;/strong&gt; to see if it would reduce the memory footprint of our web app. To speed up the development, we found a library that wraps up the IndexedDB API into something easy to use: &lt;a href="https://dexie.org/" rel="noopener noreferrer"&gt;Dexie.js&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  React + IndexedDB: Event-Driven Rendering
&lt;/h3&gt;

&lt;p&gt;Since IndexedDB is &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction/complete_event" rel="noopener noreferrer"&gt;&lt;em&gt;event-based&lt;/em&gt;&lt;/a&gt;, we decided to follow a similar architecture in our frontend.&lt;/p&gt;

&lt;p&gt;On a macro view, here are the major functions we have implemented:&lt;/p&gt;
&lt;h4&gt;
  
  
  Global data fetching
&lt;/h4&gt;

&lt;p&gt;Every 15 seconds, data is fetched from the API and then directly stored in IndexedDB, from there, the JavaScript variable holding the data is freed as we don't want to pass it to a different context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;updatePilotsInIDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PilotSummarySessionResponseDto&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Clear data from 15 sec ago&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bulkPut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Insert new data&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CustomEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PILOTS_UPDATE_EVENT&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the data is persisted in IndexedDB, we dispatch a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent" rel="noopener noreferrer"&gt;JavaScript Event&lt;/a&gt; which will notify listeners that some new data is available&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One might ask why we don't use Dexie's &lt;a href="https://dexie.org/docs/dexie-react-hooks/useLiveQuery()" rel="noopener noreferrer"&gt;&lt;code&gt;useLiveQuery&lt;/code&gt;&lt;/a&gt;, it is because this copies the data and stores in a state variable until the next batch comes. Which is exactly what we are trying to avoid, as we need the data only for rendering, then we don't need it in a state anymore, until the next render. Although we still use that hook for some specific data where we filter a specific element we need, and so end up storing a single object of negligible size.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Also, some data doesn't need to be refetched every 15 seconds (e.g., list of airlines), so we took the opportunity to store it without expiration, so the visitor doesn't have to refetch it on each visit.&lt;/p&gt;

&lt;h4&gt;
  
  
  Map rendering
&lt;/h4&gt;

&lt;p&gt;Now that we have events informing us about underlying data change, we have full control on re-rendering the map with fresh data. &lt;/p&gt;

&lt;p&gt;We are trying to batch as much as possible the re-renders, so the client's browser doesn't waste compute resources for nothing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PILOTS_UPDATE_EVENT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;batchRender&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which triggers a re-render only &lt;strong&gt;once every 15 seconds&lt;/strong&gt;, while before we had a re-render per endpoint (x5) every 15 seconds! &lt;/p&gt;

&lt;h4&gt;
  
  
  Data querying
&lt;/h4&gt;

&lt;p&gt;Some features available on the website require searching for some specific objects across multiple datasets (e.g., search all live sessions for a given user ID, which spans the pilots list, ATC list, observers list).&lt;/p&gt;

&lt;p&gt;Having IndexedDB as the underlying storage layer, we can leverage their &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex" rel="noopener noreferrer"&gt;Index&lt;/a&gt; feature, which allows specifying which fields we plan on filtering-on (kinda like NoSQL databases).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dexieDB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Dexie&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webeye_db&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Dexie&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PilotSummarySessionResponseDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;atcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AtcSummarySessionResponseDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;observers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ObserverListSessionResposeDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;airlines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AirlineIDBDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;icao&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;notams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;NotamIDBDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;specialAreas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;SpecialAreaIDBDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;creators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EntityTable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;CreatorIDBDto&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;stores&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id, callsign, userId, flightPlan.arrivalId, flightPlan.departureId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;atcs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id, callsign, userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;observers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id, callsign, userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;airlines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;icao&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;notams&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id, *centerIds, *airportIcaos&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;specialAreas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id, *centerIds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;creators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we created indexes for all the search patterns we needed, which we exploited as shown in this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSessionsByVIDsFromIDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;BaseSessionDto&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;atcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;obss&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;anyOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;atcs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;anyOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;observers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;userId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;anyOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;atcs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;obss&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way we don't have to load the whole list of online users, instead we have an efficient lookup!&lt;/p&gt;

&lt;p&gt;Even on nested fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getTrafficsAtAirportInIDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;icao&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AirportTrafficsDto&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;inbound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outbound&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flightPlan.arrivalId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;icao&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nx"&gt;dexieDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pilots&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;flightPlan.departureId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;icao&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inbound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outbound&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visually, this is how it looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2wqey8wg30glq5zxb4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fs2wqey8wg30glq5zxb4a.png" alt=" " width="800" height="2509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So, where are we now ?
&lt;/h2&gt;

&lt;p&gt;Most of the time in programming, it is really hard to say if a change was &lt;em&gt;worth it&lt;/em&gt;. In our case, here what we keep from this experience:&lt;/p&gt;

&lt;p&gt;Upsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The code is a bit cleaner

&lt;ul&gt;
&lt;li&gt;We know why something re-renders because we control the events&lt;/li&gt;
&lt;li&gt;Large variables aren't passed across contexts&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Better rendering performance

&lt;ul&gt;
&lt;li&gt;Since we are batching the updates, they only happen once every 15 seconds instead of 5+ / 15s.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Better memory footprint

&lt;ul&gt;
&lt;li&gt;Large states aren't kept after their usage is done (20-30 MB lighter memory profile)&lt;/li&gt;
&lt;li&gt;When we need the complete set, we discard of the variable right after use&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Downsides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;em&gt;not-so-intuitive&lt;/em&gt; approach to React apps. 

&lt;ul&gt;
&lt;li&gt;Using event-driven updates in React isn't typical at all, some maintainers might get lost&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Overall, the experience for our users has improved and allowed us to store persistent data directly in their browser to reduce the requests when reopening the page.&lt;/p&gt;

&lt;p&gt;What do you think about our approach ? What would you have done differently ?&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>programming</category>
      <category>indexeddb</category>
    </item>
  </channel>
</rss>
