<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Blogs by Luciano Nooijen</title><link>https://lucianonooijen.com/blog/</link><description>Recent content in Blogs by Luciano Nooijen</description><language>en</language><lastBuildDate>Wed, 16 Apr 2025 00:00:00 +0300</lastBuildDate><atom:link href="https://lucianonooijen.com/blog/index.xml" rel="self" type="application/rss+xml"/><item><title>Creating and validating JWTs with public/private keys (JSON Web Key Set, JWKS)</title><link>https://lucianonooijen.com/blog/creating-jwt-with-public-private-keys-jwk/</link><pubDate>Wed, 16 Apr 2025 00:00:00 +0300</pubDate><guid>https://lucianonooijen.com/blog/creating-jwt-with-public-private-keys-jwk/</guid><description>&lt;p>&lt;em>This article discusses an implementation of JWT generation in Go, and validation in Go and Typescript. The library used for this, jose, has implementations for many other programming languages, so the principles can be applied in nearly all commonly used programming languages.&lt;/em>&lt;/p>
&lt;p>JSON Web Token&lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup> (JWT for short) is a very commonly used way to create and validate authentication tokens on the web. This article is not a tutorial of JWT basics, there are many resources available for that online already.&lt;/p>
&lt;p>Most often, when generating JWTs, a secret value is used to create the third part of the JWT, the signature. This approach is great when there is a single service that generates and validates tokens. In some cases though, you want to sign a token on one service, and have it validated by another service. This could be achieved by sharing the signing secret, but this is not an optimal solution in terms of security, you would rather sign a JWT on one service and then validate it on another service, in a way that doesn&amp;rsquo;t require you to have access to the secret value.&lt;/p>
&lt;p>This can be achieved by using private/public key encryption. You can sign a JWT with a private key, and in the header include the link to where the public key can be found to validate the signature. This will allow other services to validate a JWT, without requiring access to the actual secret value to sign a key. To make this more concrete: this system is used by for example Auth0&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> to sign keys to then allow you to validate these tokens without needing access to the signing secret. This system is also used by AWS Cognito&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>I recently integrated this into &lt;a
href="https://capsa.gg"
target="_blank" rel="noopener"
>Capsa.gg&lt;/a
>
. The web panel has middleware to validate JWTs, which will redirect a user to log in if the key is invalid. When I started implementing this, I was not able to find a lot of resources on this unfortunately and it took quite some trial-and-error to complete. Yet in the end, implementing JWTs with private key signing and public key validation was quite straight-forward. Hence why I&amp;rsquo;m sharing my findings here, in the hope it will be useful to others.&lt;/p>
&lt;p>This article will implement JWT generation with a private key and gives an example of validating with the private key. Some code is omitted, a working example project can be found on Github: &lt;a
href="https://github.com/lucianonooijen/jwt-public-private-key-demo"
target="_blank" rel="noopener"
>lucianonooijen/jwt-public-private-key-demo&lt;/a
>
. This also contains some unit tests for validation, which are not included in this article.&lt;/p>
&lt;blockquote class="alert alert-caution">
&lt;div class="alert-heading">
&lt;p class="alert-heading-emoji">
❗
&lt;/p>
&lt;p class="alert-heading-text">
Security disclaimer
&lt;/p>
&lt;/div>
&lt;p>Please don&amp;rsquo;t follow this guide as if it&amp;rsquo;s gospel, make sure you know what you are doing. This article and the example project are meant as starting points, to be adopted according to specific needs. Never blindly trust code on the internet, especially when it comes to security. The footnotes of this article contain trusted sources that can be used for further reading.&lt;/p>
&lt;/blockquote>
&lt;h2 id="json-web-key">JSON Web Key&lt;/h2>
&lt;p>When generating a JWT, the first part of the token, the header, contains information about how the key is signed. The most important field for our use-case, is the algorithm field &lt;code>alg&lt;/code>.&lt;/p>
&lt;p>For example, when signing a token with just a simple secret, the header contents are&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;alg&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;HS256&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;typ&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;JWT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>indicating that the algorithm used is &amp;ldquo;HMAC using SHA-256&amp;rdquo;&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>. This is the easiest way to sign and verify JWTs, used in many projects. You need a secret to sign the token, and use the same token to validate it.&lt;/p>
&lt;p>Now let&amp;rsquo;s look at the header of a JWT that has been signed with the public/private key setup that this article discusses:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-json" data-lang="json">&lt;span style="display:flex;">&lt;span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;alg&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;RS256&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;jku&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;http://localhost:4000/.well-known/jwks.json&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">&amp;#34;typ&amp;#34;&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;JWT&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The algorithm field here now has a different value, &lt;code>RS256&lt;/code>, indicating the digital signature algorithm used is &amp;ldquo;RSASSA-PKCS1-v1_5 using SHA-256&amp;rdquo;&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>This looks like a cat walked across the keyboard, but we can break this down. &lt;code>RSA&lt;/code> indicates that we are using RSA, so a public/private key setup, &lt;code>SSA&lt;/code> indicates &amp;ldquo;With Appendix&amp;rdquo;, meaning the signature is separate from the message, which makes sense with how JWTs work. &lt;code>PKCS1&lt;/code> refers to the &amp;ldquo;Public-Key Cryptography Standards #1&amp;rdquo;&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>, which defines how RSA should be used. &lt;code>v1_5&lt;/code> indicates the padding scheme, which is older, but used and supported in JWTs.&lt;/p>
&lt;p>A new field in the header, is the &lt;code>jku&lt;/code> field. This is the JWK Set URL&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>, often being &lt;code>&amp;lt;service_url&amp;gt;/.well-known/jwks.json&lt;/code>&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>. The URL here should resolve to the public key data when making a GET request to this URL over TLS&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>. The value of this field should not be blindly trusted at runtime unless it matches an expected value, or there is the risk of a key injection attack. When fetching this URL, there is a specific format how the data should be encoded&lt;sup id="fnref:10">&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref">10&lt;/a>&lt;/sup>, which is called the JSON Web Key Set, or JWKS. &amp;ldquo;Set&amp;rdquo; here meaning a collection of multiple JSON Web Keys (singular: JWK). We will discuss this a bit more later. The important takeaway here is: this URL contains the public key data of all signing keys that should be considered valid.&lt;/p>
&lt;p>A small note to add: for the example project, we are not fully following the RFC, as we fetch the JWKS without TLS. When implementing a JWKS endpoint, it should always be to over TLS to follow the RFC&lt;sup id="fnref1:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>To summarize: when implementing public/private key signing for JWTs, the JWT should contain the correct algorithm, as well as a URL where valid keys can be retrieved for validating the tokens. There is a specific format on how the public keys must be made available.&lt;/p>
&lt;!--
mmdc -i mermaid.mmd -o static/img/0011-1.png -b transparent
```mermaid
sequenceDiagram
participant A as Auth
participant S as Service
participant U as User
par Authentication
U->>A: Request JWT via login
A->>U: Sends JWT, signed with private key
end
par AuthenticatedRequest
U->>S: Sends request with JWT
opt Fetch JWKS
S->>A: Request JWKS
A->>S: Send JWKS
end
S- ->S: Validates JWT validity using JWKS || TODO: REMOVE SPACE
S->>U: Sends response based on JWT validity
end
```
-->
&lt;p>&lt;img src="https://lucianonooijen.com/img/0011-1.png" alt="jwt with jwks sequence diagram">&lt;/p>
&lt;h2 id="publicprivate-key-generation">Public/private key generation&lt;/h2>
&lt;p>So, let&amp;rsquo;s start implementing JWT signing with public/private keys. The first thing we need, is to actually generate the keys. We need to generate keys with at least 2048 bits&lt;sup id="fnref:11">&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref">11&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>We don&amp;rsquo;t want to generate a key, keep it in memory and use that right away. The keys should be reused if the application starts up. You can generate the key in any way you like, this is an example of how to do this in Go. Let&amp;rsquo;s assume we want to create a private key, then extract the public key from that, encode that as strings that we will return to the calling function, which will write the keys to disk.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">token&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;crypto/rand&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;crypto/rsa&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;crypto/x509&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;encoding/pem&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// RsaKeySet is a set of generated public and private keys.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">RsaKeySet&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">PrivateKey&lt;/span> []&lt;span style="color:#66d9ef">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">PublicKey&lt;/span> []&lt;span style="color:#66d9ef">byte&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// GenerateRsaKeySet generates a private/public key set for signing JWKs.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">GenerateRsaKeySet&lt;/span>() (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">RsaKeySet&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKey&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">GenerateKey&lt;/span>(&lt;span style="color:#a6e22e">rand&lt;/span>.&lt;span style="color:#a6e22e">Reader&lt;/span>, &lt;span style="color:#ae81ff">4096&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error generating private key: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">EncodePrivateKeyToBytes&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// EncodePrivateKeyToBytes takes in a *rsa.PrivateKey and encodes this to the private and public PEM key bytes.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">EncodePrivateKeyToBytes&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">RsaKeySet&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKeyBytes&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">encodePrivateKeyToPEM&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">publicKeyBytes&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">encodePublicKeyToPem&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error generating public key: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">keyData&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">RsaKeySet&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">PrivateKey&lt;/span>: &lt;span style="color:#a6e22e">privateKeyBytes&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">PublicKey&lt;/span>: &lt;span style="color:#a6e22e">publicKeyBytes&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">keyData&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">encodePrivateKeyToPEM&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>) []&lt;span style="color:#66d9ef">byte&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Get PKCS #1, ASN.1 DER format&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKeyContents&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">x509&lt;/span>.&lt;span style="color:#a6e22e">MarshalPKCS1PrivateKey&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// pem.Block&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateBlock&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pem&lt;/span>.&lt;span style="color:#a6e22e">Block&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;RSA PRIVATE KEY&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Headers&lt;/span>: &lt;span style="color:#66d9ef">nil&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Bytes&lt;/span>: &lt;span style="color:#a6e22e">privateKeyContents&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Private key in PEM format&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privatePEM&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pem&lt;/span>.&lt;span style="color:#a6e22e">EncodeToMemory&lt;/span>(&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">privateBlock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">privatePEM&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">encodePublicKeyToPem&lt;/span>(&lt;span style="color:#a6e22e">privateKey&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>) ([]&lt;span style="color:#66d9ef">byte&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Extract the public key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">publicKey&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">privateKey&lt;/span>.&lt;span style="color:#a6e22e">PublicKey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Marshal the public key to PKIX, ASN.1 DER form&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">publicKeyBytes&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">x509&lt;/span>.&lt;span style="color:#a6e22e">MarshalPKIXPublicKey&lt;/span>(&lt;span style="color:#a6e22e">publicKey&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error marshaling public key: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Create the PEM block&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">publicKeyPEMBlock&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pem&lt;/span>.&lt;span style="color:#a6e22e">Block&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;PUBLIC KEY&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Bytes&lt;/span>: &lt;span style="color:#a6e22e">publicKeyBytes&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Encode the PEM block to []byte&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">publicKeyPEM&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pem&lt;/span>.&lt;span style="color:#a6e22e">EncodeToMemory&lt;/span>(&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">publicKeyPEMBlock&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">publicKeyPEM&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;em>Security disclaimer: keys must be handled with care. You need to use different keys for different environments and have proper practices for handling secrets. Please make sure you handle key generation and storage with great care.&lt;/em>&lt;/p>
&lt;h2 id="accessing-the-public-and-private-key">Accessing the public and private key&lt;/h2>
&lt;p>So we have generated a private and public key and persisted the contents to disk. Let&amp;rsquo;s now work on the actual JWT logic that uses these keys. In practice, we don&amp;rsquo;t need to load the public key from disk, only the private key, which we will use to get the public key values.&lt;/p>
&lt;p>So let&amp;rsquo;s write two helper functions: one that will load the private key from disk, and another that will decode a base64 string that is passed into it, for example if the private key value is set as an environment variable, base64 encoded.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">token&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;crypto/rsa&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;crypto/x509&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;encoding/base64&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;encoding/pem&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;errors&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;os&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">blockTypeRsaPrivateKey&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;RSA PRIVATE KEY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">var&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ErrorPemPrivateKeyDecoding indicates that the decoding of the private-key containing PEM block failed.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ErrorPemPrivateKeyDecoding&lt;/span> = &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;failed to decode PEM block containing private key&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// LoadPrivateKeyFromPath reads the private key from a file and returns a rsa.PrivateKey struct.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">LoadPrivateKeyFromPath&lt;/span>(&lt;span style="color:#a6e22e">path&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Read the file&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">keyBytes&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">os&lt;/span>.&lt;span style="color:#a6e22e">ReadFile&lt;/span>(&lt;span style="color:#a6e22e">path&lt;/span>) &lt;span style="color:#75715e">//nolint:gosec // &amp;#34;G304: Potential file inclusion via variable&amp;#34;, this path is set in the config.yml and should never contain user input&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;failed to read private key file: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Decode the PEM block&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">block&lt;/span>, &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pem&lt;/span>.&lt;span style="color:#a6e22e">Decode&lt;/span>(&lt;span style="color:#a6e22e">keyBytes&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">block&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#a6e22e">block&lt;/span>.&lt;span style="color:#a6e22e">Type&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">blockTypeRsaPrivateKey&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">ErrorPemPrivateKeyDecoding&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Parse the private key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKey&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">x509&lt;/span>.&lt;span style="color:#a6e22e">ParsePKCS1PrivateKey&lt;/span>(&lt;span style="color:#a6e22e">block&lt;/span>.&lt;span style="color:#a6e22e">Bytes&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;failed to parse private key: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">privateKey&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// LoadPrivateKeyFromBase64String reads the private key from a base64 string and returns a rsa.PrivateKey struct.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">LoadPrivateKeyFromBase64String&lt;/span>(&lt;span style="color:#a6e22e">b64&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">keyBytes&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">base64&lt;/span>.&lt;span style="color:#a6e22e">StdEncoding&lt;/span>.&lt;span style="color:#a6e22e">DecodeString&lt;/span>(&lt;span style="color:#a6e22e">b64&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;failed to decode private key base64 string: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Decode the PEM block&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">block&lt;/span>, &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pem&lt;/span>.&lt;span style="color:#a6e22e">Decode&lt;/span>(&lt;span style="color:#a6e22e">keyBytes&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">block&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#a6e22e">block&lt;/span>.&lt;span style="color:#a6e22e">Type&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">blockTypeRsaPrivateKey&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">ErrorPemPrivateKeyDecoding&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Parse the private key&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKey&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">x509&lt;/span>.&lt;span style="color:#a6e22e">ParsePKCS1PrivateKey&lt;/span>(&lt;span style="color:#a6e22e">block&lt;/span>.&lt;span style="color:#a6e22e">Bytes&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;failed to parse private key: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">privateKey&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>We now have our &lt;code>*rsa.PrivateKey&lt;/code> instance again that we can use to sign JWTs with.&lt;/p>
&lt;h2 id="signing-jwts">Signing JWTs&lt;/h2>
&lt;p>To sign the JWTs, we will be using go-jose&lt;sup id="fnref:12">&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref">12&lt;/a>&lt;/sup>. Let&amp;rsquo;s create a struct called &lt;code>Token&lt;/code>, which holds the data we need, and exposes methods to sign and validate tokens:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;crypto/rsa&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;errors&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/go-jose/go-jose/v4&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">Token&lt;/span> &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKey&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">jwk&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">JSONWebKey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">signer&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">Signer&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With our &lt;code>Token&lt;/code> struct defined, let&amp;rsquo;s create a function called &lt;code>New&lt;/code> in our package, that will accept a private key and returns an initialized &lt;code>Token&lt;/code> instance:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">const&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">keyID&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;jwt-demo-server-jwk&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">algorithm&lt;/span> = &lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">RS256&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// New returns a Token instance after validating the *rsa.PrivateKey.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">pk&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">rsa&lt;/span>.&lt;span style="color:#a6e22e">PrivateKey&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Token&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">pk&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;private key argument is required&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pk&lt;/span>.&lt;span style="color:#a6e22e">Validate&lt;/span>(); &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error validating private key: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">jwk&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">JSONWebKey&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Key&lt;/span>: &lt;span style="color:#a6e22e">pk&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Use&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;sig&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Algorithm&lt;/span>: string(&lt;span style="color:#a6e22e">algorithm&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">KeyID&lt;/span>: &lt;span style="color:#a6e22e">keyID&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">signerOptions&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> (&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">SignerOptions&lt;/span>{}).
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">WithHeader&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;alg&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">algorithm&lt;/span>).
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">WithHeader&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;typ&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;JWT&amp;#34;&lt;/span>).
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">WithHeader&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;jku&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;http://localhost:4000/.well-known/jwks.json&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">jwtSigner&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">NewSigner&lt;/span>(&lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">SigningKey&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Algorithm&lt;/span>: &lt;span style="color:#a6e22e">algorithm&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Key&lt;/span>: &lt;span style="color:#a6e22e">pk&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }, &lt;span style="color:#a6e22e">signerOptions&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error creating jwt signer: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">jwkInstance&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">Token&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">privateKey&lt;/span>: &lt;span style="color:#a6e22e">pk&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">jwk&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">jwk&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">signer&lt;/span>: &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">jwtSigner&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">jwkInstance&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>A few important notes here: the &lt;code>keyID&lt;/code> is an identifier for which type of key we are dealing with. If you have two token types: a user and an admin, you want to use different key IDs, which other services should validate if it&amp;rsquo;s the right &lt;code>keyID&lt;/code>. The &lt;code>jku&lt;/code> field is now hardcoded in this example, you will most likely need to add a method that uses the application config to generate the correct URL, which is using &lt;code>https&lt;/code>, not &lt;code>http&lt;/code> like in this example. In the &lt;code>jose.JSONWebKey&lt;/code>, we set &lt;code>Use: &amp;quot;sig&amp;quot;&lt;/code>, because we use this key for signing, not for encrypting the JWT&lt;sup id="fnref:13">&lt;a href="#fn:13" class="footnote-ref" role="doc-noteref">13&lt;/a>&lt;/sup>, that is beyond the scope of this article.&lt;/p>
&lt;p>With our new &lt;code>Token&lt;/code> instance, let&amp;rsquo;s add a few methods that allows us to actually generate and validate JWTs. For our example application, we will only have a single token type. When dealing with multiple token types, you will most likely have to generate multiple functions for generation and validation, for each token type.&lt;/p>
&lt;p>Let&amp;rsquo;s generate a JWT:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">const&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Audience&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;example&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Issuer&lt;/span> = &lt;span style="color:#e6db74">&amp;#34;jwt-demo-server&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Token&lt;/span>) &lt;span style="color:#a6e22e">GenerateJwt&lt;/span>(&lt;span style="color:#a6e22e">subject&lt;/span>, &lt;span style="color:#a6e22e">jwtId&lt;/span>, &lt;span style="color:#a6e22e">name&lt;/span>, &lt;span style="color:#a6e22e">role&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#66d9ef">string&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">now&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Now&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">expiryHours&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Duration&lt;/span>(&lt;span style="color:#ae81ff">48&lt;/span>) &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#a6e22e">time&lt;/span>.&lt;span style="color:#a6e22e">Hour&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">claims&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">JwtClaims&lt;/span>{
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Issuer&lt;/span>: &lt;span style="color:#a6e22e">Issuer&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Subject&lt;/span>: &lt;span style="color:#a6e22e">subject&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Audience&lt;/span>: &lt;span style="color:#a6e22e">Audience&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Expiry&lt;/span>: &lt;span style="color:#a6e22e">now&lt;/span>.&lt;span style="color:#a6e22e">Add&lt;/span>(&lt;span style="color:#a6e22e">expiryHours&lt;/span>).&lt;span style="color:#a6e22e">Unix&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">NotBefore&lt;/span>: &lt;span style="color:#a6e22e">now&lt;/span>.&lt;span style="color:#a6e22e">Unix&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">IssuedAt&lt;/span>: &lt;span style="color:#a6e22e">now&lt;/span>.&lt;span style="color:#a6e22e">Unix&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">JwtID&lt;/span>: &lt;span style="color:#a6e22e">jwtId&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Name&lt;/span>: &lt;span style="color:#a6e22e">name&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Role&lt;/span>: &lt;span style="color:#a6e22e">role&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">tok&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">generateTokenForClaims&lt;/span>(&lt;span style="color:#a6e22e">claims&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error signing token: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;generated client jwt with subject: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">subject&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">tok&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// generateTokenForClaims is a function that will sign the JwtClaims passed in.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// WARNING: This method should only be called in public wrapper functions and not exposed directly.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// For generating tokens in production code, always use the audience-specific methods.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Token&lt;/span>) &lt;span style="color:#a6e22e">generateTokenForClaims&lt;/span>(&lt;span style="color:#a6e22e">claims&lt;/span> &lt;span style="color:#a6e22e">JwtClaims&lt;/span>) (&lt;span style="color:#66d9ef">string&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) { &lt;span style="color:#75715e">//nolint:gocritic // Jose needs val, not ref&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">jwt&lt;/span>.&lt;span style="color:#a6e22e">Signed&lt;/span>(&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">signer&lt;/span>).&lt;span style="color:#a6e22e">Claims&lt;/span>(&lt;span style="color:#a6e22e">claims&lt;/span>).&lt;span style="color:#a6e22e">Serialize&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The scary looking second function performs the logic to actually generate the JWT string with the claims passed in. Discussing the JWT claims is beyond the scope of this article, these are explained in the RFC&lt;sup id="fnref1:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. An example can be found &lt;a
href="https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/server/internal/token/claims.go"
target="_blank" rel="noopener"
>in the example project&lt;/a
>
. The note &amp;ldquo;For generating tokens in production code, always use the audience-specific methods.&amp;rdquo; refers to using wrapper functions to generate keys with the correct data.&lt;/p>
&lt;h2 id="validating-jwts-using-the-private-key">Validating JWTs using the private key&lt;/h2>
&lt;p>Of course we also want to be able to validate the JWT on the service that generated the key. Let&amp;rsquo;s add that before we implement the validation on an external service:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">var&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ErrorJwtParsing indicates that the signature of the JWT is not valid.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ErrorJwtParsing&lt;/span> = &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;parsing JWT failed&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ErrorJwtValidation indicates that the token claims could not be validated.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ErrorJwtValidation&lt;/span> = &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;claim validation for JWT failed&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// ErrorJwtConversion indicates that the token claims to struct conversion failed.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ErrorJwtConversion&lt;/span> = &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;claim conversion for JWT failed&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Token&lt;/span>) &lt;span style="color:#a6e22e">ValidateJwt&lt;/span>(&lt;span style="color:#a6e22e">token&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">JwtClaims&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">parsedToken&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">jwt&lt;/span>.&lt;span style="color:#a6e22e">ParseSigned&lt;/span>(&lt;span style="color:#a6e22e">token&lt;/span>, []&lt;span style="color:#a6e22e">jose&lt;/span>.&lt;span style="color:#a6e22e">SignatureAlgorithm&lt;/span>{&lt;span style="color:#a6e22e">algorithm&lt;/span>})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error parsing signed token: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">ErrorJwtParsing&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">claims&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">Claims&lt;/span>{}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Note: dereference here is very important!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// The jose code checks for *rsa.PublicKey specifically, and does not accept rsa.PublicKey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">parsedToken&lt;/span>.&lt;span style="color:#a6e22e">Claims&lt;/span>(&lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">privateKey&lt;/span>.&lt;span style="color:#a6e22e">PublicKey&lt;/span>, &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">claims&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error validating token claims: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">ErrorJwtValidation&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">claims&lt;/span>.&lt;span style="color:#a6e22e">jwtClaims&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error converting token claims: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">ErrorJwtConversion&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">validateJwtClaims&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">log&lt;/span>.&lt;span style="color:#a6e22e">Printf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error validating token claims: %s\n&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">c&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This code assumes a &lt;code>type Claims map[string]any&lt;/code> definition. On this type, we add a method &lt;code>jwtClaims&lt;/code> that converts this map to our own custom claims type, which was mentioned before. For inspiration, you can check &lt;a
href="https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/server/internal/token/claims.go"
target="_blank" rel="noopener"
>this example&lt;/a
>
. We also call our own &lt;code>validateJwtClaims&lt;/code> method, which is beyond the scope of this article, but it validates a bunch of fields, you can draw inspiration from &lt;a
href="https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/server/internal/token/jwt_validate_claims.go"
target="_blank" rel="noopener"
>this example&lt;/a
>
. The validation required depends on your application. You should also validate that the algorithm and key id match the expected values, or reject the token if they don&amp;rsquo;t.&lt;/p>
&lt;h2 id="sharing-the-public-key">Sharing the public key&lt;/h2>
&lt;p>Remember that when signing tokens with RS256 (public/private key setup), that we should include a URL where the JWKS can be retrieved? Let&amp;rsquo;s add that now.&lt;/p>
&lt;p>In our &lt;code>Token&lt;/code> instance, we want to add a method that allows us to retrieve the JSON representation of the public key. Getting the JSON representation of the public key is very straight-forward:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// GetPublicKey takes the public part of the private key and marshals this to JSON for the .well-known/jwks.json endpoint.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">t&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">Token&lt;/span>) &lt;span style="color:#a6e22e">GetPublicKey&lt;/span>() ([]&lt;span style="color:#66d9ef">byte&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// SECURITY NOTE: NEVER REMOVE `.Public()` FROM THIS CODE&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Otherwise, the private key will be shared and the whole token system will be compromised!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">t&lt;/span>.&lt;span style="color:#a6e22e">jwk&lt;/span>.&lt;span style="color:#a6e22e">Public&lt;/span>().&lt;span style="color:#a6e22e">MarshalJSON&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Make sure you use &lt;code>jwk.Public()&lt;/code>!&lt;/strong> If you do not include the &lt;code>Public()&lt;/code> part in here, you will expose the full &lt;em>private&lt;/em> key, meaning the whole token system will be compromised!&lt;/p>
&lt;p>We can add a simple handler to return this key like this, assuming we have our handlers in a &lt;code>handlers&lt;/code> struct which has an instance of &lt;code>Token&lt;/code> in the &lt;code>token&lt;/code> struct field:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">h&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">handlers&lt;/span>) &lt;span style="color:#a6e22e">Jwk&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">gin&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">pubKey&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">h&lt;/span>.&lt;span style="color:#a6e22e">token&lt;/span>.&lt;span style="color:#a6e22e">GetPublicKey&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// handle error&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">Header&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;content-type&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;application/json&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Manually writing JSON for compatibility with the []byte from pubKey&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">Writer&lt;/span>.&lt;span style="color:#a6e22e">WriteString&lt;/span>(&lt;span style="color:#e6db74">`{&amp;#34;keys&amp;#34;:[`&lt;/span>) &lt;span style="color:#75715e">//nolint:errcheck,gosec // This is fine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">Writer&lt;/span>.&lt;span style="color:#a6e22e">Write&lt;/span>(&lt;span style="color:#a6e22e">pubKey&lt;/span>) &lt;span style="color:#75715e">//nolint:errcheck,gosec // This is fine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">Writer&lt;/span>.&lt;span style="color:#a6e22e">WriteString&lt;/span>(&lt;span style="color:#e6db74">`]}`&lt;/span>) &lt;span style="color:#75715e">//nolint:errcheck,gosec // This is fine&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">Status&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">StatusOK&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The proper JSON marshalling is left as an exercise for the reader.&lt;/p>
&lt;h2 id="inspecting-the-jwt">Inspecting the JWT&lt;/h2>
&lt;p>Let&amp;rsquo;s add a super simple API endpoint &lt;code>GET /jwt&lt;/code> that generates a JWT with some hardcoded values. This of course is just for demo purposes and not secure to be used in production. In production, add authentication checks for users to log in and use the proper JWT claim values.&lt;/p>
&lt;p>Example handler for testing:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> (&lt;span style="color:#a6e22e">h&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">handlers&lt;/span>) &lt;span style="color:#a6e22e">GetJwt&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">gin&lt;/span>.&lt;span style="color:#a6e22e">Context&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Never do this in production, just for demo!&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">jwt&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">h&lt;/span>.&lt;span style="color:#a6e22e">token&lt;/span>.&lt;span style="color:#a6e22e">GenerateJwt&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;42&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;1337&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;John Doe&amp;#34;&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;Example&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Handle error&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Validate the token passes our own validation, plus get the claims so we can return the expiry time&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">claims&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">h&lt;/span>.&lt;span style="color:#a6e22e">token&lt;/span>.&lt;span style="color:#a6e22e">ValidateJwt&lt;/span>(&lt;span style="color:#a6e22e">jwt&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Handle error&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">JSON&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">StatusOK&lt;/span>, &lt;span style="color:#66d9ef">struct&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Token&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span> &lt;span style="color:#e6db74">`json:&amp;#34;token&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">Expiry&lt;/span> &lt;span style="color:#66d9ef">int64&lt;/span> &lt;span style="color:#e6db74">`json:&amp;#34;expiry&amp;#34;`&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }{&lt;span style="color:#a6e22e">Token&lt;/span>: &lt;span style="color:#a6e22e">jwt&lt;/span>, &lt;span style="color:#a6e22e">Expiry&lt;/span>: &lt;span style="color:#a6e22e">claims&lt;/span>.&lt;span style="color:#a6e22e">Expiry&lt;/span>}) &lt;span style="color:#75715e">// You should do this properly&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>So to test, let&amp;rsquo;s run the server and run &lt;code>curl http://localhost:4000/jwt | jq '.token'&lt;/code>&lt;sup id="fnref:14">&lt;a href="#fn:14" class="footnote-ref" role="doc-noteref">14&lt;/a>&lt;/sup> to generate a token, and parse the JWT string from the response. Let&amp;rsquo;s copy the value into &lt;a
href="https://jwt.io"
target="_blank" rel="noopener"
>jwt.io&lt;/a
>
. We can see that the header values and claims are set correctly.&lt;/p>
&lt;h2 id="validating-the-jwt-in-nextjs">Validating the JWT in NextJS&lt;/h2>
&lt;p>So now we get to the exciting part, validating the JWT in a separate service. Let&amp;rsquo;s initialize an empty NextJS project and add some JWT validation middleware:&lt;/p>
&lt;p>middleware.ts:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">deleteJwtCookie&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">getJwtCookieFromRequest&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>} &lt;span style="color:#66d9ef">from&lt;/span> &lt;span style="color:#e6db74">&amp;#34;@/data/jwt/cookiesServer&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> &lt;span style="color:#a6e22e">JwtValidator&lt;/span> &lt;span style="color:#66d9ef">from&lt;/span> &lt;span style="color:#e6db74">&amp;#34;@/server/jwt&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> { &lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">NextRequest&lt;/span>, &lt;span style="color:#a6e22e">NextResponse&lt;/span> } &lt;span style="color:#66d9ef">from&lt;/span> &lt;span style="color:#e6db74">&amp;#34;next/server&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// This middleware checks if users are logged in.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// This is not implemented for security reasons, as that is done on the server.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// It simply serves as a way to make sure users don&amp;#39;t get 4xx errors.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">export&lt;/span> &lt;span style="color:#66d9ef">async&lt;/span> &lt;span style="color:#66d9ef">function&lt;/span> &lt;span style="color:#a6e22e">middleware&lt;/span>(&lt;span style="color:#a6e22e">req&lt;/span>: &lt;span style="color:#66d9ef">NextRequest&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> { &lt;span style="color:#a6e22e">pathname&lt;/span>, &lt;span style="color:#a6e22e">search&lt;/span>, &lt;span style="color:#a6e22e">origin&lt;/span>, &lt;span style="color:#a6e22e">basePath&lt;/span> } &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">req&lt;/span>.&lt;span style="color:#a6e22e">nextUrl&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">path&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">`&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">basePath&lt;/span>&lt;span style="color:#e6db74">}${&lt;/span>&lt;span style="color:#a6e22e">pathname&lt;/span> &lt;span style="color:#f92672">===&lt;/span> &lt;span style="color:#e6db74">&amp;#34;/&amp;#34;&lt;/span> &lt;span style="color:#f92672">?&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span> &lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#a6e22e">pathname&lt;/span>&lt;span style="color:#e6db74">}${&lt;/span>&lt;span style="color:#a6e22e">search&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">`&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">logBase&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#e6db74">`[middleware][&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">req&lt;/span>.&lt;span style="color:#a6e22e">method&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74"> &lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">path&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">]:`&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">token&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">await&lt;/span> &lt;span style="color:#a6e22e">getJwtCookieFromRequest&lt;/span>(&lt;span style="color:#a6e22e">req&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">isAuthenticated&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">token&lt;/span> &lt;span style="color:#f92672">?&lt;/span> &lt;span style="color:#66d9ef">await&lt;/span> &lt;span style="color:#a6e22e">JwtValidator&lt;/span>.&lt;span style="color:#a6e22e">ValidateJwt&lt;/span>(&lt;span style="color:#a6e22e">token&lt;/span>) &lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#66d9ef">false&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">isAuthRoute&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">req&lt;/span>.&lt;span style="color:#a6e22e">nextUrl&lt;/span>.&lt;span style="color:#a6e22e">pathname&lt;/span>.&lt;span style="color:#a6e22e">startsWith&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;/auth&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">logBase&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;received request, isAuthenticated&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">isAuthenticated&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;| isAuthRoute&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">isAuthRoute&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Logged-in users accessing login routes should be redirected to the homepage
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">isAuthenticated&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#a6e22e">isAuthRoute&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">logBase&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;redirecting logged in user from auth route to&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">req&lt;/span>.&lt;span style="color:#a6e22e">url&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">NextResponse&lt;/span>.&lt;span style="color:#a6e22e">redirect&lt;/span>(&lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">URL&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;/&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">req&lt;/span>.&lt;span style="color:#a6e22e">url&lt;/span>));
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Send users who are not logged in and request non-auth pages to the login page
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#f92672">!&lt;/span>&lt;span style="color:#a6e22e">isAuthenticated&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#f92672">!&lt;/span>&lt;span style="color:#a6e22e">isAuthRoute&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">await&lt;/span> &lt;span style="color:#a6e22e">deleteJwtCookie&lt;/span>(&lt;span style="color:#a6e22e">req&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">signInUrl&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> &lt;span style="color:#a6e22e">URL&lt;/span>(&lt;span style="color:#e6db74">`&lt;/span>&lt;span style="color:#e6db74">${&lt;/span>&lt;span style="color:#a6e22e">basePath&lt;/span>&lt;span style="color:#e6db74">}&lt;/span>&lt;span style="color:#e6db74">/auth/login`&lt;/span>, &lt;span style="color:#a6e22e">origin&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">path&lt;/span> &lt;span style="color:#f92672">!==&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&amp;#34;&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">signInUrl&lt;/span>.&lt;span style="color:#a6e22e">searchParams&lt;/span>.&lt;span style="color:#66d9ef">set&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;redirect&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">path&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">logBase&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;redirecting non logged in user to&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">signInUrl&lt;/span>.&lt;span style="color:#a6e22e">toString&lt;/span>(),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> );
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">NextResponse&lt;/span>.&lt;span style="color:#a6e22e">redirect&lt;/span>(&lt;span style="color:#a6e22e">signInUrl&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">console&lt;/span>.&lt;span style="color:#a6e22e">log&lt;/span>(&lt;span style="color:#a6e22e">logBase&lt;/span>, &lt;span style="color:#e6db74">&amp;#34;continue executing request&amp;#34;&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">NextResponse&lt;/span>.&lt;span style="color:#a6e22e">next&lt;/span>();
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">export&lt;/span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">config&lt;/span> &lt;span style="color:#f92672">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">matcher&lt;/span>&lt;span style="color:#f92672">:&lt;/span> [
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">/*
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * Match all request paths except for the ones starting with:
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * - api (API routes)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * - _next/static (static files)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * - _next/image (image optimization files)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> * - favicon.ico, sitemap.xml, robots.txt (metadata files)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e"> */&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)&amp;#34;&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> ],
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The actual &lt;code>JwtValidator&lt;/code> implementation will be specific for each application and should take care to properly validate tokens. The implementation for the demo project can be found &lt;a
href="https://github.com/lucianonooijen/jwt-public-private-key-demo/blob/main/client/server/jwt.ts"
target="_blank" rel="noopener"
>here&lt;/a
>
. For validation, you should think of checking the algorithm, expected audience, issuer and timestamps.&lt;/p>
&lt;p>It is good practice to set the JWKS endpoint in your application config, so that you only fetch keys from that location and reject keys that have a different &lt;code>jku&lt;/code> field in the header. Don&amp;rsquo;t trust the &lt;code>jku&lt;/code> field from the header and start fetching the key from there, in case a malicious actor set up their own service to sign keys. Security here is very important, so take good care to minimize the possible attack vectors.&lt;/p>
&lt;h2 id="seeing-this-in-action">Seeing this in action&lt;/h2>
&lt;p>The example project for this article can be found &lt;a
href="https://github.com/lucianonooijen/jwt-public-private-key-demo"
target="_blank" rel="noopener"
>here&lt;/a
>
on GitHub.&lt;/p>
&lt;p>For a more complete example with different types of keys, you can take a look at &lt;a
href="https://github.com/capsa-gg/capsa/tree/main/server/internal/infrastructure/token"
target="_blank" rel="noopener"
>Capsa&amp;rsquo;s token package&lt;/a
>
and at &lt;a
href="https://github.com/capsa-gg/capsa/blob/main/web/middleware.ts"
target="_blank" rel="noopener"
>Capsa&amp;rsquo;s NextJS middleware&lt;/a
>
.&lt;/p>
&lt;blockquote class="alert alert-caution">
&lt;div class="alert-heading">
&lt;p class="alert-heading-emoji">
❗
&lt;/p>
&lt;p class="alert-heading-text">
Disclaimer
&lt;/p>
&lt;/div>
&lt;p>Never blindly trust code on the internet, especially when it comes to security. Use this article as a starting point but please do your own research and familiarize yourself with the relevant RFCs when implementing this for a production application.&lt;/p>
&lt;/blockquote>
&lt;br />
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7519"
target="_blank" rel="noopener"
>RFC7519&lt;/a
>
&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&amp;#160;&lt;a href="#fnref1:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>&lt;a
href="https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets"
target="_blank" rel="noopener"
>Auth0 JSON Web Key Sets&lt;/a
>
&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>&lt;a
href="https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html#amazon-cognito-user-pools-using-tokens-aws-jwt-verify"
target="_blank" rel="noopener"
>AWS Cognito Token Validation&lt;/a
>
&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.2"
target="_blank" rel="noopener"
>RFC7518: HMAC with SHA-2 Functions&lt;/a
>
&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.1"
target="_blank" rel="noopener"
>RFC7519: Digital Signature with RSASSA-PKCS1-v1_5&lt;/a
>
&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc8017"
target="_blank" rel="noopener"
>RFC8017&lt;/a
>
&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7515#section-4.1.2"
target="_blank" rel="noopener"
>RFC7515: &amp;ldquo;jku&amp;rdquo; (JWK Set URL) Header Parameter&lt;/a
>
&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&amp;#160;&lt;a href="#fnref1:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>This is not a strict requirement for our use-case here, but comes from the conventions for OAuth2.0 Authorization Server Metadata. This is also the endpoint used by Auth0 and AWS Cognito. See &lt;a
href="https://datatracker.ietf.org/doc/html/rfc8414#section-2"
target="_blank" rel="noopener"
>RFC8414: Authorization Server Metadata&lt;/a
>
for more info.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>The RFC marks this field as optional but for our use-case we want to include this field.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:10">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7517#section-4"
target="_blank" rel="noopener"
>RFC7517: JSON Web Key (JWK) Format&lt;/a
>
&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:11">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.3"
target="_blank" rel="noopener"
>RFC7518: Digital Signature with RSASSA-PKCS1-v1_5&lt;/a
>
&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:12">
&lt;p>&lt;a
href="https://github.com/go-jose/go-jose/"
target="_blank" rel="noopener"
>go-jose/go-jose&lt;/a
>
&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:13">
&lt;p>&lt;a
href="https://datatracker.ietf.org/doc/html/rfc7517#section-4.2"
target="_blank" rel="noopener"
>RFC7517: &amp;ldquo;use&amp;rdquo; (Public Key Use) Parameter&lt;/a
>
&amp;#160;&lt;a href="#fnref:13" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:14">
&lt;p>jq is a very helpful tool for working with JSON data on the command line, but how to use it are beyond the scope of this article.&amp;#160;&lt;a href="#fnref:14" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Why I stopped using AI code editors</title><link>https://lucianonooijen.com/blog/why-i-stopped-using-ai-code-editors/</link><pubDate>Tue, 01 Apr 2025 00:00:00 +0300</pubDate><guid>https://lucianonooijen.com/blog/why-i-stopped-using-ai-code-editors/</guid><description>&lt;p>&lt;em>TL;DR: I chose to make using AI a manual action, because I felt the slow loss of competence over time when I relied on it, and I recommend everyone to be cautious with making AI a key part of their workflow.&lt;/em>&lt;/p>
&lt;p>In late 2022, I used AI tools for the first time, even before the first version of ChatGPT. In 2023, I started using AI-based tools in my development workflow. Initially, I was super impressed with the capabilities of these LLMs. The fact that I could just copy and paste obscure compiler errors along with the C++ source code, and be told where the error is caused felt like magic.&lt;/p>
&lt;p>Once GitHub Copilot started becoming more and more powerful, I started using it more and more. I used various other LLM integrations right in my editor. Using AI was part of my workflow.&lt;/p>
&lt;p>In late 2024 I removed all LLM integrations from my code editors. I still use LLMs occasionally and I do think AI can be used in a way that is very beneficial for many programmers. So then why don&amp;rsquo;t I use AI-powered code editing tools?&lt;/p>
&lt;h2 id="tesla-fsd">Tesla FSD&lt;/h2>
&lt;p>From 2019 to 2021 I drove a Tesla. Though I would never make the same purchase again, not for political reasons, just because the cars are quite low quality, very overpriced and a hell to repair or maintain.&lt;/p>
&lt;p>When I got my Tesla, I started using the Full Self-Driving (FSD) anytime I could. It felt great to just put the car on FSD on the highway and zone out a bit. Switching lanes was as simple as hitting the turn signal, and the car would switch lanes. Driving for me was just getting to the highway, turning on FSD, telling the car to switch lanes every now and then, and listen to music/podcasts while zoning out.&lt;/p>
&lt;p>If you drive a car often, you&amp;rsquo;ll know that when you&amp;rsquo;re driving on the highway, everything sort of happens automatically. Keeping your car in the lane at the right speed becomes a passive action, it does not require the type of focus that for example reading a book requires, it&amp;rsquo;s the type of focus that walking requires, it happens in the background of your mind.&lt;/p>
&lt;p>In the period from 2019 to 2021 I exclusively drove my Tesla for longer rides. After 2021, I went back to driving regular cars and making this switch was definitely not what I expected. Driving on the highway required my full attention for the first month or so, I had to re-learn keeping the car in the middle of the lane without thinking about it.&lt;/p>
&lt;p>Being reliant on Tesla&amp;rsquo;s FSD took away my own ability to go into autopilot.&lt;/p>
&lt;h2 id="my-experience-with-ai-code-editors">My experience with AI code editors&lt;/h2>
&lt;p>Working with AI-powered code editors was somewhat similar. Initially, I felt that I completed work a lot faster when assisted by AI. The work I was doing most of the time was not super complex, and AI felt like putting my Tesla on FSD, I could just guide the machine to do my work for me.&lt;/p>
&lt;p>In my free time, I started working on a side project on my personal account on my work device. On this account, I did not have access to Copilot and my other cool, fancy AI tools. This is when using AI started to feel very similar to my Tesla FSD story.&lt;/p>
&lt;p>I felt less competent at doing what was quite basic software development than a year or so before. All of a sudden, it made it very clear to me how reliant I had become on AI tools. Anytime I defined a function, I paused in my editor to wait until the AI tools would write the implementation for me. It took some effort to remember what the syntax was to write unit tests by hand.&lt;/p>
&lt;p>With my work, AI started to become less useful over time as well. Not only did it take out the fun for me, but I started to feel a bit insecure about making some implementation decisions myself. Outsourcing the decisions to the AI seemed a lot easier. But sometimes, the AI couldn&amp;rsquo;t figure things out, even with the best prompts. It was quite clear that because I did not practice the basics often, I was less capable with the harder parts as well.&lt;/p>
&lt;h2 id="the-loss-of-fingerspitzengefühl">The loss of Fingerspitzengefühl&lt;/h2>
&lt;blockquote>
&lt;p>&lt;strong>&lt;em>Fingerspitzengefühl&lt;/em>&lt;/strong> [ˈfɪŋɐˌʃpɪtsənɡəˌfyːl] is a German term, literally meaning &lt;em>&amp;ldquo;finger tips feeling&amp;rdquo;&lt;/em> and meaning intuitive flair or instinct, which has been adopted by the English language as a loanword. It describes a great situational awareness, and the ability to respond most appropriately and tactfully. &lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>&lt;/p>
&lt;/blockquote>
&lt;p>Defining seniority is a very tough thing. Though in my opinion a lot of being a &amp;ldquo;senior&amp;rdquo; is in soft-skills, when it comes to the technical hard-skills, a lot comes down to Fingerspitzengefühl. The longer you work with a language, framework or codebase, the more you develop this kind of intuition of what the correct approach is. The gut feeling of &amp;ldquo;something feels off&amp;rdquo; slowly turns into a feeling of &amp;ldquo;this is what we should do&amp;rdquo;.&lt;/p>
&lt;p>This developed intuition is not just on an architectural level. A big component is in the lower level details, when to use pointers (or what type of pointers), whether to use asserts or checks, what to pick from the standard library when multiple options are available (though senior C++ programmers still can&amp;rsquo;t seem to agree on this).&lt;/p>
&lt;p>This intuition is what I was slowly losing when relying on AI tools a lot. And this is coming from a lead developer. When I see a lot of hype about vibe coding, I can&amp;rsquo;t help but think: how do you exactly expect to vibe code your way to senior? Where will you get the skills from to maintain and extend the vibe-coded codebase when the AI tools are down, or have become too expensive?&lt;/p>
&lt;p>Even with larger context windows, more computing power, reasoning models or agents, there will be things that AI won&amp;rsquo;t be able to do. Over time, the AI tools will be more and more powerful, sure. But when you receive a Slack message that &amp;ldquo;the website works fine, but the app is down in production; I tried it locally and there it works fine, nothing in Sentry either&amp;rdquo;, good luck getting an AI agent to fix this for you. Maybe it can, maybe it can&amp;rsquo;t. And when an AI agent can&amp;rsquo;t figure it out, will your reply be &amp;ldquo;sorry, Cursor doesn&amp;rsquo;t get it, will prompt more tomorrow&amp;rdquo;?&lt;/p>
&lt;h2 id="you-can-get-by-without-these-tools">You can get by without these tools&lt;/h2>
&lt;p>Sometimes it feels like you have to use AI or be out of a job in 6 months. We&amp;rsquo;ve been hearing the &amp;ldquo;3-6 months from now&amp;rdquo;-story for over two years at this point. I stopped trusting CEO promises about functionality &amp;ldquo;3-6 months from now&amp;rdquo; years ago. When I got my Tesla in 2019, I paid €6400 for functionality that was supposed to arrive in &amp;ldquo;3-6 months from now&amp;rdquo;, and the functionality is still not present the way it was promised over 5 years ago.&lt;/p>
&lt;p>Right now, it is unlikely that letting AI do your coding will work for projects larger than a university project. When working on legacy systems or larger projects in enterprises or when you need to work with and consult a lot of dependency internals (like I do with Unreal Engine), AI tools will often not be able to make things work. When you need to work with internal DSLs, tools or frameworks, good luck getting LLMs to generate useful output. For some industries, you can&amp;rsquo;t even use AI tools at all for a multitude of reasons.&lt;/p>
&lt;p>For some things you really should not &lt;em>want&lt;/em> to rely on AI. When implementing authentication systems like JWT&lt;sup id="fnref:2">&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref">2&lt;/a>&lt;/sup> signing or RBAC&lt;sup id="fnref:3">&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref">3&lt;/a>&lt;/sup>, adding &amp;ldquo;and it should be secure&amp;rdquo; to the prompt won&amp;rsquo;t make it secure if it&amp;rsquo;s been trained on GitHub code that had CVEs&lt;sup id="fnref:4">&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref">4&lt;/a>&lt;/sup>. When it comes to security, you should be the person who is responsible and understands this fully. Critical systems should be written and reviewed by humans, if we are heading to a situation where one AI agent writes the code, another reviews the autogenerated PR and then another AI agent deploys the code, we will see a huge spike of security issues soon.&lt;/p>
&lt;h2 id="where-i-draw-the-line">Where I draw the line&lt;/h2>
&lt;p>I still use AI, sometimes. I think it can be a great tool, when used wisely. I draw the line at integration. I keep AI fully separate from my code editor. All of the context, I add manually. I intentionally keep the effort required quite high, so it disincentivizes me.&lt;/p>
&lt;p>Examples where I use AI for work include &amp;ldquo;convert these Go tests in structs to tests in a map&amp;rdquo;, &amp;ldquo;convert this calculation to SIMD&amp;rdquo;, or &amp;ldquo;when the content type is application/zlib, decode the body&amp;rdquo;&lt;sup id="fnref:5">&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref">5&lt;/a>&lt;/sup>. I have set up some custom instructions to only give me the code that has changed, and give me instructions for adding it. This way, I am still the one making the changes in the codebase. Just approving a Git diff is not enough, I want to manually add the code myself, only then do I feel confident to sign off on it and take responsibility for it.&lt;/p>
&lt;p>Another great use case for AI is learning. I often have questions that are quite uncommon, as I have a few very niche interests. Turns out, adding netcode to a custom game engine using ECS doesn&amp;rsquo;t have a lot of learning resources. What has worked for me, is asking AI to explain pieces of code, like &amp;ldquo;explain this assembly code&amp;rdquo;, &amp;ldquo;explain what this shader does&amp;rdquo;, &amp;ldquo;which books go in-depth about resolving client/server desyncs in game engines&amp;rdquo;. The AI seems to struggle with these sometimes, I&amp;rsquo;m getting mixed results, but the results are still much better than search engines. I will even use it for this article, though not for writing content, but for checking&lt;sup id="fnref:6">&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref">6&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Another benefit of using AI this way is the cost. No unnecessary API calls, manually managed contexts and more control over the LLM settings. I use a desktop application with a bunch of different LLMs hooked up to it. I have used it daily for the last 3 months or so, and in total, I have consumed around $4 in credits.&lt;/p>
&lt;p>I do want to add that with some things I am more strict. On my personal website, I don&amp;rsquo;t want any AI-generated content, whether that&amp;rsquo;s text or images. I don&amp;rsquo;t like AI generated images or &amp;lsquo;art&amp;rsquo; personally for various reasons and I think AI-generated text lacks character, it feels very flat and boring. When something is created by humans, it to me has more value than when it&amp;rsquo;s created by AI.&lt;/p>
&lt;h2 id="doing-what-you-love">Doing what you love&lt;/h2>
&lt;p>It is also worth noting that there are more things to think about than efficiency and productivity. It&amp;rsquo;s also about doing what you love. If you love coding, keep doing it yourself, even if a computer might be better at it.&lt;/p>
&lt;p>In 1997, Deep Blue won the chess match against the then world chess champion Garry Kasparov&lt;sup id="fnref:7">&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref">7&lt;/a>&lt;/sup>, yet people still play chess. When it comes to programming, I&amp;rsquo;d say that I program for the same reason that people still play chess&lt;sup id="fnref:8">&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref">8&lt;/a>&lt;/sup>. Though chess and software development are very different, with chess being much more limited in scope, I think it is good to keep in mind that sometimes, we can do things just to enjoy them.&lt;/p>
&lt;h2 id="my-advice-to-new-programmers">My advice to new programmers&lt;/h2>
&lt;p>Don&amp;rsquo;t become a forever junior who lets AI do all their work. If you want to become a programmer, learn to program yourself. Be curious, put in the time and effort to learn how things really work, and how things work in the layer below that. It really pays off. Learning how everything works under the hood and using that is amazing, just keep learning, don&amp;rsquo;t be a prompt engineer (if you can even call that engineering). Believe me, it&amp;rsquo;s more fun to be competent&lt;sup id="fnref:9">&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref">9&lt;/a>&lt;/sup>.&lt;/p>
&lt;p>Even though AI might be smarter than you, never blindly trust the AI output. Don&amp;rsquo;t build your whole workflow around it. Sometimes try to work without it for a few days. The better at programming you are, the more AI will get in your way for the more complex work.&lt;/p>
&lt;p>If you learn to code now, keep building your skills instead of letting AI do all the heavy lifting, you&amp;rsquo;ll be capable of fixing the messes that vibe coding is now creating. I don&amp;rsquo;t want to sound elitist, but if you don&amp;rsquo;t want to learn to go beyond vibe coding, maybe coding isn&amp;rsquo;t for you. Because positions where all work can be done by vibe coding are the ones that will be eliminated first when AI becomes more powerful.&lt;/p>
&lt;p>And remember: if you cannot code without AI, you cannot code.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>When you are using AI, you are sacrificing knowledge for speed. Sometimes it&amp;rsquo;s worth making this trade-off. Though it is important to remember that even the best athletes in the world are still doing their basic drills for a reason. The same applies to software development: you need to practice the basics, to be able to do the advanced work. You need to keep your axe sharp.&lt;/p>
&lt;p>We are still a long way out from AI taking over our jobs. A lot of companies are creating FOMO&lt;sup id="fnref:10">&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref">10&lt;/a>&lt;/sup> as a sales tactic to get more customers, to show traction to their investors, to get another round of funding, to generate the next model that will definitely revolutionize everything.&lt;/p>
&lt;p>AI is a tool, it is not good or bad in itself, it&amp;rsquo;s what you do with it. I do think it can be a great tool, as long as you are not reliant on it for your workflow. Make sure you can still work effectively without it, make sure you don&amp;rsquo;t push code to production that you don&amp;rsquo;t fully understand and don&amp;rsquo;t think of AI as a replacement for your own thinking. Stay curious, keep learning.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>Source: &lt;a
href="https://en.wikipedia.org/wiki/Fingerspitzengef%C3%BChl"
target="_blank" rel="noopener"
>Wikipedia&lt;/a
>
&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:2">
&lt;p>JSON Web Tokens, or JWTs are a common way to generate authentication tokens, among other uses&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:3">
&lt;p>Role-based access control (RBAC) is a mechanism to restrict system access by setting permissions and privileges&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:4">
&lt;p>Common Vulnerabilities and Exposures (CVE) is a program used to identify, define and catalog publicly disclosed cybersecurity vulnerabilities, &lt;a
href="https://www.cve.org/"
target="_blank" rel="noopener"
>cve.org&lt;/a
>
&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:5">
&lt;p>The specific contents here don&amp;rsquo;t matter that much, they are just examples of what I use AI for&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:6">
&lt;p>The prompt I&amp;rsquo;ve used for this article: &amp;ldquo;I want you to proofread an article I have written. I want you to give me feedback on incorrect grammar or broken sentences, using UK grammar. Do not comment on sentences that should be broken up or things that could be improved just slightly, only real errors. Do not return modified sentences, but point out where the issue is, under which paragraph, in which sentence and what the mistake is. I will make the required changes myself&amp;rdquo;. It came back with a few typos, like &amp;ldquo;form&amp;rdquo; that should be &amp;ldquo;from&amp;rdquo;, &amp;ldquo;eb&amp;rdquo; that should be &amp;ldquo;be&amp;rdquo;.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:7">
&lt;p>Source: &lt;a
href="https://www.ibm.com/history/deep-blue"
target="_blank" rel="noopener"
>IBM History&lt;/a
>
&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:8">
&lt;p>Source: &lt;a
href="https://youtu.be/-eS5-kaTSD0?si=Mf3ySN8QbpWhgLgK&amp;amp;t=328"
target="_blank" rel="noopener"
>Tsoding on YouTube&lt;/a
>
&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:9">
&lt;p>Source: &lt;a
href="https://youtu.be/mTa2d3OLXhg?si=vfjLD1DeoPZMERxA&amp;amp;t=1126"
target="_blank" rel="noopener"
>DHH in an interview on YouTube&lt;/a
>
&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;li id="fn:10">
&lt;p>Fear of missing out&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Announcing Capsa, an Unreal Engine logging solution</title><link>https://lucianonooijen.com/blog/announcing-capsa/</link><pubDate>Mon, 24 Feb 2025 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/announcing-capsa/</guid><description>&lt;p>&lt;em>tl;dr: Capsa makes Unreal Engine logging a lot better, especially for those working on multiplayer. Check it out &lt;a
href="https://capsa.gg/blog/announcing-capsa"
target="_blank" rel="noopener"
>here&lt;/a
>
.&lt;/em>&lt;/p>
&lt;p>I&amp;rsquo;m very excited to share what I&amp;rsquo;ve been working on over the last few months by announcing the v0.1 release of Capsa.&lt;/p>
&lt;h2 id="introducing-capsa">Introducing Capsa&lt;/h2>
&lt;p>&lt;em>An open-source Unreal Engine logging solution.&lt;/em>&lt;/p>
&lt;p>Ever since I started working on multiplayer games, I was quite surprised how few tools were available for working effectively with Unreal Engine logs for multiplayer games. After working on various titles, Mark Jawdoszak and I started building a tool called Capsa to address this. We&amp;rsquo;ve made this fully open-source so it can be used by other teams facing similar challenges.&lt;/p>
&lt;p>More in-depth reasons why we&amp;rsquo;ve built Capsa and licensing can be found in &lt;a
href="https://capsa.gg/blog/why-we-built-capsa/"
target="_blank" rel="noopener"
>this blog post on Capsa.gg&lt;/a
>
.&lt;/p>
&lt;h2 id="about-capsa">About Capsa&lt;/h2>
&lt;p>Capsa makes this easy by automatically collecting logs and making them accessible in a structured, searchable, and shareable way.&lt;/p>
&lt;p>Some of the features Capsa offers:&lt;/p>
&lt;ul>
&lt;li>Logs with syntax highlighting and sharable links&lt;/li>
&lt;li>Filter, search, and link logs&lt;/li>
&lt;li>Merged client-server logs&lt;/li>
&lt;li>Minimal setup, no game core or engine changes&lt;/li>
&lt;li>Cloud-native, so host anywhere&lt;/li>
&lt;li>Optimized for performance&lt;/li>
&lt;li>Open-source, web stack AGPL3.0 and UE plugin MIT&lt;/li>
&lt;li>Dark-mode support&lt;/li>
&lt;/ul>
&lt;p>Working on Capsa has been a blast and we&amp;rsquo;ve got tons of ideas of new features we want to add over the next few months.&lt;/p>
&lt;p>To read the full announcement post, check out &lt;a
href="https://capsa.gg/blog/announcing-capsa"
target="_blank" rel="noopener"
>Announcing Capsa&lt;/a
>
.&lt;/p>
&lt;h2 id="building-capsa">Building Capsa&lt;/h2>
&lt;p>Some notes on building Capsa and the tech we have used.&lt;/p>
&lt;h4 id="extending-unreal-engine">Extending Unreal Engine&lt;/h4>
&lt;p>We chose to extend Unreal Engine rather than replace functionality. We are hooking into &lt;code>GLog&lt;/code> as an output device, so we can capture &lt;code>UE_LOG&lt;/code> calls and we don&amp;rsquo;t require game code changes with a new log system. We want to have a system that is easy to add to and easy to remove from games, and I think this is a great way to achieve that.&lt;/p>
&lt;p>The received log lines are formatted according to our &lt;a
href="https://capsa.gg/docs/technical/protocol"
target="_blank" rel="noopener"
>Capsa log format&lt;/a
>
so the server can parse it correctly. This way we don&amp;rsquo;t change any default Unreal Engine log output, but we receive the correct format on the server&lt;/p>
&lt;h4 id="golang-server-log-parsing">Golang server log parsing&lt;/h4>
&lt;p>The log is processed in O(1) complexity, reading one character at a time. A buffer is built with all line characters until encountering a &lt;code>\n&lt;/code>. The line buffer is then used to extract metadata for that specific line. The metadata is stored in the database and the log chunks are stored in S3-compatible storage.&lt;/p>
&lt;p>Due to the per-character algorithm for log chunk processing, it allows the server to be very performant with extracting the metadata. At the time of writing this, parsing a 100k line log chunk (which is much bigger than will be done in production) takes just around 60ms.&lt;/p>
&lt;p>More details on log processing can be found on &lt;a
href="https://capsa.gg/docs/technical/log-processing"
target="_blank" rel="noopener"
>Log processing&lt;/a
>
.&lt;/p>
&lt;h4 id="server-side-merging-and-filtering">Server-side merging and filtering&lt;/h4>
&lt;p>We had to make an important decision on doing log merging and filtering, whether we wanted to do this on the server or client. There are pros and cons for each, but we decided to do this server-side.&lt;/p>
&lt;p>By doing this, we can re-use the existing log parsing code for incoming logs, get better performance, but it means that changing the filtering/merging settings requires reloading the full log from the server. We figured we needed server-side logic anyway, as large logs cannot be kept fully in the browser&amp;rsquo;s memory, so at least for v0.1, all of the merging and filtering logic is handled server-side.&lt;/p>
&lt;p>Because we store per-chunk metadata, we can ignore chunks that don&amp;rsquo;t fit the filtering criteria as well, giving us a good performance boost. The benchmark for filtering lines from a 100.000-line chunk for streaming to a user (as is used for the admin panel) is around 70ms.&lt;/p>
&lt;h4 id="golang-server-sql">Golang server SQL&lt;/h4>
&lt;p>For handling database logic, I chose to go with sqlc and golang-migrate, as I have written in my &lt;a
href="./best-golang-sql-handling"
>Golang SQL handling article&lt;/a
>
. This has been a good decision so far, as I have full control over the SQL to optimize for performance. I have already done a few optimizations in the queries, which would not have been possible with ORMs.&lt;/p>
&lt;h4 id="jwt-with-privatepublic-keys">JWT with private/public keys&lt;/h4>
&lt;p>Authentication is hand-rolled in the API server, so there is no reliance on external parties for this. JWTs are used for this. Instead of using a signing secret, a private/public keypair is used.&lt;/p>
&lt;p>This allows the web panel to check the JWTs (stored as a cookie) if they are valid and have users reauthenticate if they are not.&lt;/p>
&lt;p>All API requests are done from the browser directly to the API server to not unnecessarily increase server load for the web panel. Yet we still want to validate the JWTs on page load to make sure we don&amp;rsquo;t perform API requests that will return in a &lt;code>401&lt;/code> or &lt;code>403&lt;/code> response.&lt;/p>
&lt;h4 id="building-capsa-cloud-native">Building Capsa cloud-native&lt;/h4>
&lt;p>Another concern when building Capsa was making it possible to deploy anywhere. This is why I have chosen to build the web stack with Golang and NextJS. Deploying these to any Linux or Windows system is very trivial. Both of the applications are available as Docker images and can be hosted anywhere by setting some environment variables.&lt;/p>
&lt;h4 id="log-processing-web-worker">Log processing web worker&lt;/h4>
&lt;p>The web panel uses a web worker to process incoming logs, to not block the main thread, which would make the website feel a lot slower due. This is achieved by writing a class to manage the web worker, which is then exposed in a React hook to work with the rest of the logic.&lt;/p>
&lt;h4 id="runtime-json-validation">Runtime JSON validation&lt;/h4>
&lt;p>For the web panel, we have added runtime type validation by using Zod. The React code uses &lt;code>useSWR&lt;/code> to call API endpoints as hooks. Instead of using API endpoints directly, we have added wrappers for requests, for example &lt;code>const { trigger, isMutating } = useAddTitle();&lt;/code> to add a new game title. This hook internally calls the API endpoint with &lt;code>useSWR&lt;/code>, validates the JSON it gets back with Zod, and sets an error if it&amp;rsquo;s not valid.&lt;/p>
&lt;p>By doing this, we can avoid runtime type exceptions.&lt;/p>
&lt;h2 id="continuing-development">Continuing development&lt;/h2>
&lt;p>We have tons of ideas on how to improve or extend the current functionality or new features we can add. But before we add this, we want to make sure we are on the right path with Capsa.&lt;/p>
&lt;p>Now v0.1 is released, we will be integrating this into a few Companion Group projects and gather feedback from there, as well as early-adopters. We will continue to fix bugs and work on existing and new features over time, to slowly work towards a v1.0 release.&lt;/p>
&lt;p>For v1.0 we have a few feature ideas that would make Capsa even more powerful to improve the lives of developers working on Unreal Engine multiplayer games. We are working on a few proof-of-concepts for this, though these will take some time as we need to figure out details, as we don&amp;rsquo;t want Capsa to take over game codebases, we want to keep Capsa non-intrusive.&lt;/p>
&lt;h2 id="want-to-become-an-early-adopter">Want to become an early adopter?&lt;/h2>
&lt;p>If you are interested in becoming an early adopter, the details on getting started are in &lt;a
href="https://capsa.gg/blog/announcing-capsa"
target="_blank" rel="noopener"
>Announcing Capsa&lt;/a
>
. You can also reach out to me if you need any help, I&amp;rsquo;m more than happy to help you get started with Capsa.&lt;/p>
&lt;p>All feedback - positive or negative - is very much welcome as well!&lt;/p></description></item><item><title>Why Baldur's Gate 3 is a masterpiece</title><link>https://lucianonooijen.com/blog/why-baldurs-gate-3-is-a-masterpiece/</link><pubDate>Sun, 23 Feb 2025 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/why-baldurs-gate-3-is-a-masterpiece/</guid><description>&lt;p>&lt;em>This article does not contain spoilers.&lt;/em>&lt;/p>
&lt;p>I should preface this by saying that I have never played Dungeons and Dragons in my life, and knew very little about it before playing Baldur&amp;rsquo;s Gate 3. I started playing the game because it was recommended to me by my partner and I was somewhat skeptical about it, because it was so foreign to me. It was nothing like I expected, in the best way possible.&lt;/p>
&lt;p>I am not a huge gamer. Even though I work on games, I tend to enjoy that more than actually playing games. I am quite picky with the games I play. There are quite a lot of games that I have enjoyed a ton, but nothing comes close to Baldur&amp;rsquo;s Gate 3, it is by far the best game I have ever played.&lt;/p>
&lt;p>My first playthrough took me around 80 hours, where I took my time but didn&amp;rsquo;t do most side-quests (which I didn&amp;rsquo;t even know existed). My partner&amp;rsquo;s playthrough is, at the time of writing, 196 hours long and we&amp;rsquo;re only now moving into the end game &lt;sup id="fnref:1">&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref">1&lt;/a>&lt;/sup>. I have also started a few playthroughs to explore different classes and approaches to the game at higher difficulties. It will be a long time before I&amp;rsquo;m done playing this game.&lt;/p>
&lt;p>For brevity, I&amp;rsquo;ll abbreviate Baldur&amp;rsquo;s Gate 3 to BG3 going forward.&lt;/p>
&lt;h2 id="endless-choice-and-replayability">Endless choice and replayability&lt;/h2>
&lt;p>Having choices is of course a core part of RPGs, but BG3 takes this to almost an extreme. You don&amp;rsquo;t feel forced to take a particular path in your playthrough and there are no predefined paths you can take.&lt;/p>
&lt;p>Choices you make early on can have a lot of influence on later parts of the game. The options you are given are not just to get a particular response from an NPC, but actually have a huge impact on the world in later acts.&lt;/p>
&lt;p>No two playthroughs are the same. You can play as a murderous psychopath, a charming bard, or even as a cat. Even if you pick the infamous stealth archer min-max build, it&amp;rsquo;s highly likely two playthroughs will be radically different, even when you follow an Honor Mode guide to complete the game.&lt;/p>
&lt;p>Romancing characters is done incredibly well and offers a ton of choices as well. You cannot romance all characters at once, and romancing a different companion offers even more value to start another playthrough.&lt;/p>
&lt;p>On the topic of romance, I feel obliged to mention the &amp;lsquo;bear scene&amp;rsquo; (if you know, you know). The fact that Larian took the time to add this as an option to me is incredible, regardless of which choice you made if you encountered this, the fact it&amp;rsquo;s there is already amazing to me.&lt;/p>
&lt;h2 id="cohesiveness-of-the-story">Cohesiveness of the story&lt;/h2>
&lt;p>It&amp;rsquo;s incredible to me how cohesive the story is. With so many different ways to do missions, get somewhere or handle situations you would think there would be many inconsistencies later in the game.&lt;/p>
&lt;p>Sure, there might be some tiny inconsistencies here and there, but to me it&amp;rsquo;s amazing how a game where the story is not linear apart from the three Acts can make the story of any playthrough feel like it was written to be played this way.&lt;/p>
&lt;p>The overarching story of BG3 has some of the best storytelling that I have ever seen in a game, even better than most movies or TV series. Everything comes together, there are many plot twists that all make sense.&lt;/p>
&lt;p>Everything in BG3 is interconnected. There are no parts of the story or world that feel out-of-place. Random NPCs you encounter in Act 1 will often reappear later in the game (unless you kill them for fun).&lt;/p>
&lt;p>Having a game with such freedom of choice with a better, more cohesive story than most TV series and movies is nothing short of amazing.&lt;/p>
&lt;h2 id="character-development">Character development&lt;/h2>
&lt;p>The character development in BG3 is phenomenal, for companions as well as NPCs, both with phenomenal voice acting. Even side characters you meet when making certain choices in Act 1 will show up in later acts, and have gone through some development. The development of the dream visitor is amazing as well.&lt;/p>
&lt;p>The villains in the game are so incredibly well-written. The villains are not just your everyday antagonists, they have complex personalities, there is character development for them during the game, tons of backstory and with some you can even work together if you like.&lt;/p>
&lt;p>And that is such an amazing part of the game to me as well: there is such a degree of freedom to morality. Without getting too much into the political influence of modern games, I think BG3 does an amazing job of offering choices without forcing anything upon the player, which I think is the best approach, regardless of what your personal preferences are.&lt;/p>
&lt;p>To expand a bit upon the complex personality of NPCs, one thing I dislike in some games is when all characters are super obviously &amp;lsquo;good&amp;rsquo; or &amp;lsquo;bad&amp;rsquo;. This is definitely not the case in BG3. Yes, there are some &amp;rsquo;evil&amp;rsquo; characters, but even a literal devil in the game is presented in an almost &amp;lsquo;good&amp;rsquo; way in some parts. This moral ambiguity adds another amazing layer to the story.&lt;/p>
&lt;p>The companions deserve mentioning here too, they all have their own storyline with tons of choices, will (dis)approve of your choices and their personalities can change drastically based on decisions made. A part that I have not played myself, but only seen in playthrough videos is the fact that for many characters, you can play with them as the main character, unlocking more unique content.&lt;/p>
&lt;h2 id="attention-to-detail">Attention to detail&lt;/h2>
&lt;p>The attention to detail in BG3 is absolutely insane, which shows in many ways. A great example is that there are some voice lines that you will only hear in a modded playthrough, as your character enters a scene in a state that isn&amp;rsquo;t possible in the vanilla game. Even in the vanilla game there are tons of hidden details that most players will miss.&lt;/p>
&lt;p>Larian added some details in the game that they knew less than one percent of players will ever see, some might still be undiscovered. This is such a stark contrast to some released games that feel unpolished, or some even unfinished, until a few months after release.&lt;/p>
&lt;p>A game like BG3 obviously needs a lot of failsafes, think of cases where crucial NPCs get killed, locking you out of progressing a certain quest. The failsafes in BG3 are very subtle, they don&amp;rsquo;t limit your freedom to kill certain NPCs and even the failsafes have failsafes. I have had a few nights where I binge-watched videos about the failsafes in BG3 and it&amp;rsquo;s still incredible to me how many edge cases were covered.&lt;/p>
&lt;p>The attention to detail is also clear with NPCs in the game. Nearly all NPCs have voice lines, most you can talk to, and quite a few completely random NPCs will offer some sort of side quest (that of course fit into the rest of the story really well). The banter between companions is another great touch as well.&lt;/p>
&lt;h2 id="post-launch-patches">Post-launch patches&lt;/h2>
&lt;p>To me, there is a very big difference between post-launch patches to make the game playable versus patches that improve the game. I won&amp;rsquo;t be naming any games, but there are some prominent examples of games that were released and needed a few patches (not counting the day-one patch) just to be playable. BG3 was not one of them.&lt;/p>
&lt;p>Larian did release patches after launch, but these were mostly to improve the game further and take care of community feedback. Some amazing features have been added here, like Honor Mode and the epilogue scenes. In addition, some voice lines have been replaced, they were already fine, but the fact Larian took the time to improve further on this to me is great.&lt;/p>
&lt;h2 id="mod-support">Mod support&lt;/h2>
&lt;p>Unofficial mod support was already available, but in a post-release patch, Larian added native mod support. Right now, it&amp;rsquo;s even possible to have cross-platform multiplayer sessions with mods enabled (of course with some platform restrictions). The mods add a ton of new content and possibilities, adding even more replayability to the game.&lt;/p>
&lt;h2 id="music">Music&lt;/h2>
&lt;p>I&amp;rsquo;m a huge fan of the soundtrack of BG3. The music fits the game incredibly well. The song of the boss fight with one of my favourite characters of the game, Raphael, still gives me goosebumps when I hear it. Then there&amp;rsquo;s also the fact that this character stars in his own boss song, which is amazing to me as well.&lt;/p>
&lt;h2 id="lack-of-microtransactions">Lack of microtransactions&lt;/h2>
&lt;p>One of the things BG3 does that I hope will be adopted by other games is the lack of microtransactions. There are no DLCs and no special currencies purchased with real money. You buy the game and that&amp;rsquo;s it. All post-release content was made available to all players.&lt;/p>
&lt;p>The lack of microtransactions is another way to see that BG3 was made to be an amazing game, not as a corporate cash-grab. I&amp;rsquo;m not saying BG3 could never be made with investors being involved, but the freedom Larian had regarding monetization by not having to report to investors cannot be ignored.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>Baldur&amp;rsquo;s Gate 3 has set a new gold standard for RPGs. In my opinion, this is, so far, the closest we have ever gotten to a &amp;lsquo;perfect&amp;rsquo; RPG. Whether you tend to play casual or are into min-maxing your builds at the highest difficulty, BG3 is a game worth playing, over and over.&lt;/p>
&lt;p>BG3 sets an example that I hope many games - both RPGs and other genres - will follow. There are many conclusions you can draw from BG3&amp;rsquo;s success about the games industry as a whole.&lt;/p>
&lt;p>To me, it will be a game that I will keep playing for a long time. With no DnD experience there is a ton for me to discover in the base game already, and after that there will be tons more with the official and unofficial mods.&lt;/p>
&lt;div class="footnotes" role="doc-endnotes">
&lt;hr>
&lt;ol>
&lt;li id="fn:1">
&lt;p>After 214.5h we finished the game on March 23, 2025.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink">&amp;#x21a9;&amp;#xfe0e;&lt;/a>&lt;/p>
&lt;/li>
&lt;/ol>
&lt;/div></description></item><item><title>Finding the best translation for Marcus Aurelius' Meditations</title><link>https://lucianonooijen.com/blog/best-translation-meditations/</link><pubDate>Sat, 15 Feb 2025 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/best-translation-meditations/</guid><description>&lt;p>I have read quite a lot of book on Greek and Roman philosophy and Marcus Aurelius&amp;rsquo; Meditations is the book I have read most often by far. In total, I own around 7 translations and I have read sections of other translations as well. In addition to this, I have read several secondary works, in addition to the sections of the Meditations that I have read in the original Greek. In 2021/2022, I have hosted an eight-month long weekly reading group on the Meditations as well.&lt;/p>
&lt;p>One important note about reading philosophical works, is that translation is a very important aspect if it was written in a different language. There are many features present in Ancient Greek (the language that Meditations was written in) that are not present in English, or words with no English equivalent.&lt;/p>
&lt;p>Translation is not just about translating the words but - especially with philosophy - it is also important to capture the style, tone, etc. of a text as this is essential to communicate the meaning of the source material. If that is not possible due to linguistic challenges, that should be clarified by the translator.&lt;/p>
&lt;p>So, back to Marcus Aurelius. After reading the Meditation and several secondary sources, what is my favourite translation of the work in English?&lt;/p>
&lt;h2 id="waterfields-annotated-edition">Waterfield&amp;rsquo;s Annotated Edition&lt;/h2>
&lt;p>By far the best translation of the Meditations in English is &lt;em>Robin Waterfield&amp;rsquo;s Annotated Edition&lt;/em>.&lt;/p>
&lt;p>In this translation, Waterfield does an amazing job of capturing the conciseness of the original work. The English text &amp;lsquo;feels&amp;rsquo; very similar to the original, literary, yet direct.&lt;/p>
&lt;p>In addition to the great translation, this edition has a great introduction, clarification on translating certain terminology of Greek philosophy, but to me the best part are the tons of annotations present as footnotes.&lt;/p>
&lt;p>The Meditations was not a book to be published, for Marcus Aurelius, it was most likely his &amp;ldquo;commonplace book&amp;rdquo; and Waterfield does an amazing job of adding connections to other works works and even to similar passages within the book, or simply explaining cultural references.&lt;/p>
&lt;p>If you are interested in reading Meditations, or if you have read it before but want to understand it better, you cannot go wrong with this edition.&lt;/p>
&lt;p>The ISBNs are:&lt;/p>
&lt;ul>
&lt;li>Hardcover: 9781541673854&lt;/li>
&lt;li>Paperback: 9781541673861&lt;/li>
&lt;/ul>
&lt;h2 id="other-translations-worth-checking-out">Other translations worth checking out&lt;/h2>
&lt;ul>
&lt;li>&lt;em>If you can read Ancient Greek&lt;/em>: Haine&amp;rsquo;s translation from the Loeb Classical Library is not an amazing translation, but has the original text on the left, and the English translation side-by-side on the right, which can be great.&lt;/li>
&lt;li>&lt;em>Dutch translation&lt;/em>: I have not read as many Dutch translations as English ones, but I like Simone Mooij-Valk&amp;rsquo;s &amp;ldquo;Persoonlijke notities&amp;rdquo; best so far.&lt;/li>
&lt;li>&lt;em>German Translation&lt;/em>: there is a German translation of Waterfield&amp;rsquo;s edition (Selbstbetrachtungen: Die kommentierte Edition, translated by Elisabeth Liebl), which I have partly read and quite liked.&lt;/li>
&lt;/ul>
&lt;br>
&lt;blockquote>
&lt;p>ἐξετάζειν τί μοί ἐστι νῦν ἐν τούτῳ τῷ μορίῳ, ὃ δὴ ἡγεμονικὸν καλοῦσι, καὶ τίνος ἄρα νῦν ἔχω ψυχήν;&lt;/p>
&lt;p>At this moment, what is occupying that part of me they call the command center? What kind of a soul do I actually have at the moment? (Waterfield&amp;rsquo;s translation of Meditations 5.11)&lt;/p>
&lt;/blockquote></description></item><item><title>My favourite way to handle SQL in Golang</title><link>https://lucianonooijen.com/blog/best-golang-sql-handling/</link><pubDate>Fri, 14 Feb 2025 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/best-golang-sql-handling/</guid><description>&lt;p>&lt;em>tl;dr: using sqlc and golang-migrate allows plain easy SQL in your application&lt;/em>&lt;/p>
&lt;p>Golang is a great language when building web/API services. And that makes a lot of sense, as Go was created for building scalable, performant applications. The language is very cohesive, compiles fast, has great performance and has a great ecosystem in addition to its rich standard library and great toolchain. Whenever I need to build a web service or API, I tend to reach for Go when possible, hence why I built quite a lot of applications with it so far, including around a dozen or so application back-ends.&lt;/p>
&lt;p>But this is not an article about why Go is great, but rather on how to handle SQL. Considering Golang is not made to be used in Ruby on Rails-type frameworks, you&amp;rsquo;ll most likely have to set up the SQL-logic yourself.&lt;/p>
&lt;p>My approach to SQL handling in Golang is the result of trying out a lot of approaches and seeing what works and what doesn&amp;rsquo;t. This approach is mostly aimed at building API services, backed by a database. In my case, I use Postgresql, though other SQL databases should also work with a few tweaks.&lt;/p>
&lt;p>As most applications that I build deal with quite a lot of data, I don&amp;rsquo;t want to work with ORMs like Gorm, due to the performance impact and unnecessary abstractions that it introduces. In general, I&amp;rsquo;m not the biggest fan of ORMs, as I already know SQL and don&amp;rsquo;t like learning additional tools to abstract away the actual SQL code, using SQL directly for me is simply faster, both in terms of performance as well as writing the application.&lt;/p>
&lt;p>So how then do I approach SQL in Golang?&lt;/p>
&lt;h2 id="code-generation-with-sqlc">Code generation with sqlc&lt;/h2>
&lt;p>One of the things I like about the Go ecosystem is that there are a lot of code generation tools. Code generation is something that is quite underused in my opinion, and &lt;a
href="https://sqlc.dev/"
target="_blank" rel="noopener"
>sqlc&lt;/a
>
is an amazing example of how code generation can be very powerful.&lt;/p>
&lt;p>With sqlc, you can write plain sql, and then run a command that generates the Go code in a module you can use in your application.&lt;/p>
&lt;h2 id="configuring-sqlc">Configuring sqlc&lt;/h2>
&lt;p>After &lt;a
href="https://docs.sqlc.dev/en/latest/overview/install.html"
target="_blank" rel="noopener"
>installing sqlc&lt;/a
>
on your device, getting up and running with sqlc is quite trivial. The generic installation instructions are found in the &lt;a
href="https://docs.sqlc.dev/en/latest/tutorials/getting-started-postgresql.html"
target="_blank" rel="noopener"
>sqlc docs&lt;/a
>
, but I&amp;rsquo;m using a bit of a modified configuration.&lt;/p>
&lt;p>We start with creating the &lt;code>sql/&lt;/code> and &lt;code>migrations/&lt;/code> folders, for SQL queries and database migrations.&lt;/p>
&lt;p>In the migrations directory, add a file &lt;code>0000_schema.up.sql&lt;/code> and &lt;code>0000_schema.down.sql&lt;/code> for adding your initial database schema, take this up migration for example:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">BEGIN&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Enable uuid extension
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">CREATE&lt;/span> EXTENSION &lt;span style="color:#66d9ef">IF&lt;/span> &lt;span style="color:#66d9ef">NOT&lt;/span> &lt;span style="color:#66d9ef">EXISTS&lt;/span> &lt;span style="color:#e6db74">&amp;#34;uuid-ossp&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Users table
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">CREATE&lt;/span> &lt;span style="color:#66d9ef">TABLE&lt;/span> users (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">-- User fields
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- The rest of your schema
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">COMMIT&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Make sure to add the down migrations as well. The migrations are used by sqlc to generate the database structure and add typed columns for database manipulations. They will also be used to perform actual database migrations. Wrap your migration logic in &lt;code>BEGIN&lt;/code> and &lt;code>COMMIT&lt;/code> so you can &lt;code>ROLLBACK&lt;/code> on a failed migration.&lt;/p>
&lt;h2 id="full-sqlc-config-example">Full sqlc config example&lt;/h2>
&lt;p>Here is the full configuration I&amp;rsquo;m using for the Capsa API service:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">version&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">sql&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">engine&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;postgresql&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">queries&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;sql/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">schema&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;migrations/&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">gen&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">package&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;database&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">out&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;internal/data/database&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">sql_package&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;pgx/v5&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">emit_json_tags&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">json_tags_case_style&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;camel&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">emit_pointers_for_null_types&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">overrides&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># UUID&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">db_type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;uuid&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go_type&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">import&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;github.com/google/uuid&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;UUID&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">db_type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;uuid&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">nullable&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go_type&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">import&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;github.com/google/uuid&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;UUID&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">pointer&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Timestamp&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">db_type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;pg_catalog.timestamp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">nullable&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go_type&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">import&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;time&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;Time&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">pointer&lt;/span>: &lt;span style="color:#66d9ef">true&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">db_type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;pg_catalog.timestamp&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go_type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;time.Time&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e"># Custom&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">column&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;logs_chunks.category_counts&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go_type&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">import&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;github.com/capsa-gg/capsa/server/internal/entities&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;LogChunkMetadata&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">column&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;logs_chunks.severity_counts&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">go_type&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">import&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;github.com/capsa-gg/capsa/server/internal/entities&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">type&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;LogChunkMetadata&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This configuration replaces the pgx (Postgres database driver) UUID and Timestamps with the Google and standard library implementations respectively. It also specifies a few column types to be used, so the returned data has better compatibility with my domain logic. For further explanation on this, see the &lt;a
href="https://docs.sqlc.dev/en/latest/reference/config.html"
target="_blank" rel="noopener"
>docs&lt;/a
>
.&lt;/p>
&lt;h2 id="writing-sql-statements">Writing SQL statements&lt;/h2>
&lt;p>With sqlc configured, we can write some SQL code! The &lt;a
href="https://docs.sqlc.dev/en/latest/howto/select.html"
target="_blank" rel="noopener"
>docs&lt;/a
>
explain things in more detail, but here are two very simple examples:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- name: GetUserByID :one
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#f92672">*&lt;/span> &lt;span style="color:#66d9ef">FROM&lt;/span> users
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span> id &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Gets a user by their ID. The &lt;code>*&lt;/code> return value here will use the database schema to determine the return type to make it fully typesafe.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- name: UpdateUser :one
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Update a user with optional parameters
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">UPDATE&lt;/span> users
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SET&lt;/span> first_name &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">@&lt;/span>first_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> last_name &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">@&lt;/span>last_name,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> user_role &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#f92672">@&lt;/span>user_role
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span> id &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#960050;background-color:#1e0010">$&lt;/span>&lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>RETURNING &lt;span style="color:#f92672">*&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>For updating a user, this also shows how arguments can be named with sqlc for clarity in the domain logic.&lt;/p>
&lt;h2 id="complex-sql">Complex SQL&lt;/h2>
&lt;p>The two examples above are the &amp;lsquo;hello world&amp;rsquo; equivalent of SQL statements. In larger applications things become more complex. An example of this is searching. Although there might be better ways, this is the way I have implemented search for Capsa, which is not too complex:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- name: SearchResources :many
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Searches in some database tables to find matching resources based on a &amp;#34;contains string&amp;#34; pattern (LIKE &amp;#39;%&amp;lt;arg&amp;gt;%&amp;#39;)
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WITH&lt;/span> resources &lt;span style="color:#66d9ef">AS&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;Environment&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">AS&lt;/span> &lt;span style="color:#66d9ef">table_name&lt;/span>,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">lower&lt;/span>(e.&lt;span style="color:#66d9ef">key&lt;/span>::text) &lt;span style="color:#66d9ef">AS&lt;/span> identifier,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> concat(e.name, &lt;span style="color:#e6db74">&amp;#39; environment for &amp;#39;&lt;/span>, t.name) &lt;span style="color:#66d9ef">AS&lt;/span> description,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">AS&lt;/span> details
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span> environments e
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> titles t &lt;span style="color:#66d9ef">ON&lt;/span> t.id &lt;span style="color:#f92672">=&lt;/span> e.title
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">UNION&lt;/span> &lt;span style="color:#66d9ef">ALL&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">SELECT&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#39;Logs&amp;#39;&lt;/span> ,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">lower&lt;/span>(l.log_uuid::text) ,
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> concat(l.log_type, &lt;span style="color:#e6db74">&amp;#39; log on &amp;#39;&lt;/span>, platform, &lt;span style="color:#e6db74">&amp;#39; (&amp;#39;&lt;/span>, t.name, &lt;span style="color:#e6db74">&amp;#39;, &amp;#39;&lt;/span>, e.name, &lt;span style="color:#e6db74">&amp;#39;)&amp;#39;&lt;/span>),
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">count&lt;/span>(lc)::text
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">FROM&lt;/span> logs l
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> environments e &lt;span style="color:#66d9ef">ON&lt;/span> e.id &lt;span style="color:#f92672">=&lt;/span> l.environment
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> titles t &lt;span style="color:#66d9ef">on&lt;/span> t.id &lt;span style="color:#f92672">=&lt;/span> e.title
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> logs_chunks lc &lt;span style="color:#66d9ef">ON&lt;/span> l.id &lt;span style="color:#f92672">=&lt;/span> lc.log
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">GROUP&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> l.log_uuid, l.log_type, platform, t.name, e.name
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#66d9ef">table_name&lt;/span>, identifier, description::text, details::text
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">FROM&lt;/span> resources
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span> identifier &lt;span style="color:#66d9ef">LIKE&lt;/span> &lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span> &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#66d9ef">lower&lt;/span>(&lt;span style="color:#f92672">@&lt;/span>&lt;span style="color:#66d9ef">search&lt;/span>) &lt;span style="color:#f92672">||&lt;/span> &lt;span style="color:#e6db74">&amp;#39;%&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LIMIT&lt;/span> sqlc.arg(&lt;span style="color:#e6db74">&amp;#39;limit&amp;#39;&lt;/span>);
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Another more complex example is filtering with optional arguments. The full statement is over 50 lines long, but here is the most important part of the implementation for Capsa:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-sql" data-lang="sql">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- name: ListAvailableLogs :many
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- Fetches all log chunks and aggregates an overview.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">-- LogUUID is an optional field used as a filter, which if set will return only a single result.
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">WITH&lt;/span> &lt;span style="color:#75715e">-- Omitted
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">SELECT&lt;/span> &lt;span style="color:#75715e">-- Omitted
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">FROM&lt;/span> logs l
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> cat_counts cc &lt;span style="color:#66d9ef">ON&lt;/span> cc.log &lt;span style="color:#f92672">=&lt;/span> l.id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> sev_counts sc &lt;span style="color:#66d9ef">ON&lt;/span> sc.log &lt;span style="color:#f92672">=&lt;/span> l.id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> chunk_data cd &lt;span style="color:#66d9ef">ON&lt;/span> cd.log &lt;span style="color:#f92672">=&lt;/span> l.id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> environments e &lt;span style="color:#66d9ef">on&lt;/span> l.environment &lt;span style="color:#f92672">=&lt;/span> e.id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">JOIN&lt;/span> titles t &lt;span style="color:#66d9ef">on&lt;/span> e.title &lt;span style="color:#f92672">=&lt;/span> t.id
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LEFT&lt;/span> &lt;span style="color:#66d9ef">JOIN&lt;/span> links ll &lt;span style="color:#66d9ef">on&lt;/span> l.id &lt;span style="color:#f92672">=&lt;/span> ll.&lt;span style="color:#66d9ef">source&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">WHERE&lt;/span> ( l.log_uuid &lt;span style="color:#f92672">=&lt;/span> sqlc.narg(filter_by_log_uuid) &lt;span style="color:#66d9ef">OR&lt;/span> sqlc.narg(filter_by_log_uuid) &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span> ) &lt;span style="color:#75715e">-- Optionally filter by Log UUID
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">AND&lt;/span> ( e.&lt;span style="color:#66d9ef">key&lt;/span> &lt;span style="color:#f92672">=&lt;/span> sqlc.narg(filter_by_environment)::uuid &lt;span style="color:#66d9ef">OR&lt;/span> sqlc.narg(filter_by_environment) &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span> ) &lt;span style="color:#75715e">-- Optionally filter by Environment
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">AND&lt;/span> ( l.platform &lt;span style="color:#f92672">=&lt;/span> sqlc.narg(filter_by_platform)::varchar &lt;span style="color:#66d9ef">OR&lt;/span> sqlc.narg(filter_by_platform) &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span> ) &lt;span style="color:#75715e">-- Optionally filter by Platform
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">AND&lt;/span> ( l.log_type &lt;span style="color:#f92672">=&lt;/span> sqlc.narg(filter_by_logtype) &lt;span style="color:#66d9ef">OR&lt;/span> sqlc.narg(filter_by_logtype) &lt;span style="color:#66d9ef">IS&lt;/span> &lt;span style="color:#66d9ef">NULL&lt;/span> ) &lt;span style="color:#75715e">-- Optionally filter by LogType
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#66d9ef">GROUP&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> l.id, t.name, e.name, cd.line_count, cd.chunk_count, cd.earliest_start, cd.latest_end, ll.&lt;span style="color:#66d9ef">sum&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">ORDER&lt;/span> &lt;span style="color:#66d9ef">BY&lt;/span> earliest &lt;span style="color:#66d9ef">DESC&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">LIMIT&lt;/span> &lt;span style="color:#f92672">@&lt;/span>fetchlimit::int;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will simply ignore all arguments that are not set, or include them if they are. In all honesty: the syntax with an ORM would be cleaner, but I do suspect the performance will take a hit. If performance with &lt;code>ListAvailableLogs&lt;/code> becomes an issue, I can optimize it, with ORMs, that is a lot harder.&lt;/p>
&lt;h2 id="generating-the-code">Generating the code&lt;/h2>
&lt;p>To generate the code, simply run &lt;code>sqlc generate&lt;/code> in the root of your project, and you should have your code! There are more commands you can use for analysis and linting, which are outlined in the &lt;a
href="https://docs.sqlc.dev/en/latest/howto/generate.html"
target="_blank" rel="noopener"
>docs&lt;/a
>
.&lt;/p>
&lt;h2 id="using-the-generated-code">Using the generated code&lt;/h2>
&lt;p>During the application start, you can use your config to generate the instance of the sql-generated structs:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">example&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/jackc/pgx/v5/pgxpool&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/capsa-gg/capsa/server/internal/data/database&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// NewDatabase initializes a Database instance.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">NewDatabase&lt;/span>(&lt;span style="color:#a6e22e">c&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">entities&lt;/span>.&lt;span style="color:#a6e22e">Config&lt;/span>) (&lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">database&lt;/span>.&lt;span style="color:#a6e22e">Queries&lt;/span>, &lt;span style="color:#66d9ef">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">ctx&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">context&lt;/span>.&lt;span style="color:#a6e22e">Background&lt;/span>()
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Database connection&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">dbConn&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pgxpool&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">c&lt;/span>.&lt;span style="color:#a6e22e">DatabaseConnectionString&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error opening database connection: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Ping database&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">dbConn&lt;/span>.&lt;span style="color:#a6e22e">Ping&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>, &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;error pinging database: %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Database instance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">db&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">database&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">dbConn&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">db&lt;/span>, &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>You now have a &lt;code>*database.Queries&lt;/code> instance that you can use to use query the database! Using it is as simple as&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">user&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">db&lt;/span>.&lt;span style="color:#a6e22e">GetUserByID&lt;/span>(&lt;span style="color:#a6e22e">ctx&lt;/span>, &lt;span style="color:#a6e22e">userId&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>There is also support for performing logic inside of a transaction, as outlined in the &lt;a
href="https://docs.sqlc.dev/en/latest/howto/transactions.html#using-transactions"
target="_blank" rel="noopener"
>docs&lt;/a
>
.&lt;/p>
&lt;h2 id="handling-migrations">Handling migrations&lt;/h2>
&lt;p>You should only be changing the database structure using migrations. We already have the migration scripts used by sqlc, which we can use to perform the database migrations.&lt;/p>
&lt;p>For migrations, I&amp;rsquo;m using the golang-migrate package, with a small wrapper. This is the full code of the migrator package for Capsa:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">package&lt;/span> &lt;span style="color:#a6e22e">migrator&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;database/sql&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;errors&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;fmt&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;net/http&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/golang-migrate/migrate/v4&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/golang-migrate/migrate/v4/database/pgx/v5&amp;#34;&lt;/span> &lt;span style="color:#75715e">//nolint:gocritic,stylecheck // Needs import for using pgx.WithInstance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/golang-migrate/migrate/v4/source/httpfs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Needs side effect from pgx/v5.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#e6db74">&amp;#34;github.com/golang-migrate/migrate/v4/database/pgx/v5&amp;#34;&lt;/span> &lt;span style="color:#75715e">//nolint:gocritic,stylecheck // Needs import for side effect&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#e6db74">&amp;#34;github.com/capsa-gg/capsa/server/migrations&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// Direction indicates the migration direction.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">Direction&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// UpAll and DownAll indicate the migration direction.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">const&lt;/span> (
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">UpAll&lt;/span> = &lt;span style="color:#a6e22e">Direction&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;UpAll&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">DownAll&lt;/span> = &lt;span style="color:#a6e22e">Direction&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;DownAll&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">// New returns a migrator closure that will accept UpAll or DownAll to up or down migrate the database.&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">dbConn&lt;/span> &lt;span style="color:#f92672">*&lt;/span>&lt;span style="color:#a6e22e">sql&lt;/span>.&lt;span style="color:#a6e22e">DB&lt;/span>, &lt;span style="color:#a6e22e">dbName&lt;/span> &lt;span style="color:#66d9ef">string&lt;/span>) &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">Direction&lt;/span>) &lt;span style="color:#66d9ef">error&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">func&lt;/span>(&lt;span style="color:#a6e22e">direction&lt;/span> &lt;span style="color:#a6e22e">Direction&lt;/span>) &lt;span style="color:#66d9ef">error&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Direction check&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">direction&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">UpAll&lt;/span> &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> &lt;span style="color:#a6e22e">direction&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#a6e22e">DownAll&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;migration direction should be &amp;#39;UpAll&amp;#39; or &amp;#39;DownAll&amp;#39;&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">driver&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">pgx&lt;/span>.&lt;span style="color:#a6e22e">WithInstance&lt;/span>(&lt;span style="color:#a6e22e">dbConn&lt;/span>, &lt;span style="color:#f92672">&amp;amp;&lt;/span>&lt;span style="color:#a6e22e">pgx&lt;/span>.&lt;span style="color:#a6e22e">Config&lt;/span>{})
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;invalid target postgres instance, %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Source instance for the migrations embedded in server binary&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">sourceInstance&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">httpfs&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">http&lt;/span>.&lt;span style="color:#a6e22e">FS&lt;/span>(&lt;span style="color:#a6e22e">migrations&lt;/span>.&lt;span style="color:#a6e22e">Migrations&lt;/span>), &lt;span style="color:#e6db74">&amp;#34;.&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;invalid source instance, %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Create migrator instance&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">migrator&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">migrate&lt;/span>.&lt;span style="color:#a6e22e">NewWithInstance&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;httpfs&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">sourceInstance&lt;/span>, &lt;span style="color:#a6e22e">dbName&lt;/span>, &lt;span style="color:#a6e22e">driver&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#f92672">!=&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">fmt&lt;/span>.&lt;span style="color:#a6e22e">Errorf&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;failed to initialize migrate instance, %w&amp;#34;&lt;/span>, &lt;span style="color:#a6e22e">err&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// Do the actual migration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">direction&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#a6e22e">UpAll&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">handleMigratorErrors&lt;/span>(&lt;span style="color:#a6e22e">migrator&lt;/span>.&lt;span style="color:#a6e22e">Up&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">direction&lt;/span> &lt;span style="color:#f92672">==&lt;/span> &lt;span style="color:#a6e22e">DownAll&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">handleMigratorErrors&lt;/span>(&lt;span style="color:#a6e22e">migrator&lt;/span>.&lt;span style="color:#a6e22e">Down&lt;/span>())
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#e6db74">&amp;#34;you should not see this&amp;#34;&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">func&lt;/span> &lt;span style="color:#a6e22e">handleMigratorErrors&lt;/span>(&lt;span style="color:#a6e22e">err&lt;/span> &lt;span style="color:#66d9ef">error&lt;/span>) &lt;span style="color:#66d9ef">error&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> &lt;span style="color:#a6e22e">errors&lt;/span>.&lt;span style="color:#a6e22e">Is&lt;/span>(&lt;span style="color:#a6e22e">err&lt;/span>, &lt;span style="color:#a6e22e">migrate&lt;/span>.&lt;span style="color:#a6e22e">ErrNoChange&lt;/span>) { &lt;span style="color:#75715e">// Do not report error when no database change&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#66d9ef">nil&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">err&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>With this, performing the database migrations is as simple as&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-go" data-lang="go">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">import&lt;/span> &lt;span style="color:#e6db74">&amp;#34;path/to/migrator&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">migrate&lt;/span> &lt;span style="color:#f92672">:=&lt;/span> &lt;span style="color:#a6e22e">migrator&lt;/span>.&lt;span style="color:#a6e22e">New&lt;/span>(&lt;span style="color:#a6e22e">db&lt;/span>, &lt;span style="color:#a6e22e">config&lt;/span>.&lt;span style="color:#a6e22e">DatabaseName&lt;/span>)
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">err&lt;/span> = &lt;span style="color:#a6e22e">migrate&lt;/span>(&lt;span style="color:#a6e22e">migrationDirection&lt;/span>)
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now you have good &amp;lsquo;ol plain SQL for migrations as well as database queries in Golang with code generation!&lt;/p>
&lt;h2 id="example-to-see-this-in-action">Example to see this in action&lt;/h2>
&lt;p>An example of this approach in action can be found in the &lt;a
href="https://github.com/capsa-gg/capsa/blob/main/server/"
target="_blank" rel="noopener"
>Capsa API server&lt;/a
>
. Capsa is still work in progress, but I highly doubt the SQL approach will change.&lt;/p></description></item><item><title>Easy help command for Makefiles</title><link>https://lucianonooijen.com/blog/help-makefile/</link><pubDate>Thu, 13 Feb 2025 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/help-makefile/</guid><description>&lt;p>I love Makefiles, they are my prefered way to manage builds. Not just for C or C++, I also use it for Golang for example. To me, it is the best way to manage build commands in a single place.&lt;/p>
&lt;p>As I love CLI tools in general, and working with them, the &lt;code>--help&lt;/code> flag is often a great help, but Make does not offer this out of the box, and navigating complex Makefiles can be challenging.&lt;/p>
&lt;p>I can&amp;rsquo;t remember where or when, but a few years ago, I found a snippet somewhere of a Makefile command that adds a coloured text output of all Makefile commands. Using it is as simple as making the following your first Makefile command:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Makefile" data-lang="Makefile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.PHONY&lt;/span>&lt;span style="color:#f92672">:&lt;/span> help
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">help&lt;/span>&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#75715e">## Shows all commands
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> @echo &lt;span style="color:#e6db74">&amp;#39;All Makefile commands:&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> @grep -h -E &lt;span style="color:#e6db74">&amp;#39;^[a-zA-Z_-]+:.*?## .*$$&amp;#39;&lt;/span> &lt;span style="color:#66d9ef">$(&lt;/span>MAKEFILE_LIST&lt;span style="color:#66d9ef">)&lt;/span> | awk &lt;span style="color:#e6db74">&amp;#39;BEGIN {FS = &amp;#34;:.*?## &amp;#34;}; {printf &amp;#34;\033[36m%-30s\033[0m %s\n&amp;#34;, $$1, $$2}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>After adding this, you can simply add comments after your other Makefile commands with two &lt;code>#&lt;/code> characters, like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-Makefile" data-lang="Makefile">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">.PHONY&lt;/span>&lt;span style="color:#f92672">:&lt;/span> dev
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#a6e22e">dev&lt;/span>&lt;span style="color:#f92672">:&lt;/span> CFLAGS = &lt;span style="color:#66d9ef">$(&lt;/span>CFLAGS_DEV&lt;span style="color:#66d9ef">)&lt;/span> &lt;span style="color:#75715e">## Development build for /src and /example
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>&lt;span style="color:#a6e22e">dev&lt;/span>&lt;span style="color:#f92672">:&lt;/span> clean &lt;span style="color:#66d9ef">$(&lt;/span>LIB&lt;span style="color:#66d9ef">)&lt;/span> &lt;span style="color:#66d9ef">$(&lt;/span>EXAMPLE&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>This will now be automatically generate help output when running &lt;code>make help&lt;/code>, or when you have defined this as your first Make command, with simply &lt;code>make&lt;/code>&lt;/p>
&lt;p>&lt;img src="https://lucianonooijen.com/img/0005-1.jpg" alt="make help output">&lt;/p></description></item><item><title>Hello, world! Again!</title><link>https://lucianonooijen.com/blog/hello-world-again/</link><pubDate>Wed, 12 Feb 2025 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/hello-world-again/</guid><description>&lt;p>Recently, I have had quite a lot of ideas about interesting blog posts, some short, some long; some general notes and some more technical. All in the hope that this can be helpful or at least somewhat insightful for others.&lt;/p>
&lt;p>I have updated my website to add slightly better blog support, including categories and tags to make this possible, plus better RSS feed support.&lt;/p>
&lt;p>Currently, the website is quite basic. And although I have considered giving it a bit of an overhaul with better styling, I would prefer to keep this website somewhat minimalist. I might make some changes to improve readability, but the styling itself will still be very minimalistic.&lt;/p>
&lt;p>Anyway, that&amp;rsquo;s it for now!&lt;/p></description></item><item><title>Webinar replay: common pitfalls for software start-ups and how to avoid them</title><link>https://lucianonooijen.com/blog/webinar-replay-software-start-up-pitfalls/</link><pubDate>Thu, 13 Jan 2022 00:00:00 +0200</pubDate><guid>https://lucianonooijen.com/blog/webinar-replay-software-start-up-pitfalls/</guid><description>&lt;p>On November 29th, 2021, I gave a webinar for Bytecode about common pitfalls for software start-ups and how to avoid them.&lt;/p>
&lt;p>A replay of this webinar can be seen here:&lt;/p>
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/Yl2wDvipw38" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>&lt;/iframe></description></item><item><title>Runtime JSON typechecks with Typescript interfaces</title><link>https://lucianonooijen.com/blog/runtime-json-typechecks-with-typescript-interfaces/</link><pubDate>Mon, 24 Aug 2020 00:00:00 +0300</pubDate><guid>https://lucianonooijen.com/blog/runtime-json-typechecks-with-typescript-interfaces/</guid><description>&lt;p>&lt;em>The contents of this article are technical in nature and mainly written for software developers&lt;/em>&lt;/p>
&lt;p>Bytecode is currently working on a project for a startup. We are developing an app that is linked to a backend data storage service. For this project, I wanted to have more security around the API calls and validate the types at runtime.&lt;/p>
&lt;h2 id="background">Background&lt;/h2>
&lt;p>At Bytecode, we use Typescript extensively for front-end and mobile development, to prevent Javascript&amp;rsquo;s liberal dynamic typing system from causing errors. Typescript adds an extra layer of security. In recent years, we have seen a strong decrease in type-related errors due to the use of Typescript.&lt;/p>
&lt;p>However, Typescript also has it&amp;rsquo;s limitations, the main one being that types and interfaces disappear during compile-time. So there are no run-time checks on external data that is not available during compilation, like for example API responses. Ideally, Typescript would support marshalling in a way &lt;a
href="https://medium.com/rungo/working-with-json-in-go-7e3a37c5a07b"
target="_blank" rel="noopener"
>similar to Go&lt;/a
>
, to secure type safety during runtime. Unfortunately, however, the Typescript layer disappears during compile-time and only Javascript remains: code that knows nothing about the types and interfaces defined in the source code. Unfortunately, the Go-like approach would never be possible using features built into the language.&lt;/p>
&lt;p>Until recently, we did our API calls directly within Redux actions, which didn&amp;rsquo;t cause any problems for small applications. However, this setup is not the best, considering the &lt;a
href="https://en.wikipedia.org/wiki/Single-responsibility_principle"
target="_blank" rel="noopener"
>single-responsibility principle&lt;/a
>
and keeping in mind that Redux actions can get confusing with large projects, if you&amp;rsquo;re not careful. This is why we recently switched to creating separate API packages as an abstraction layer on top of the API calls. We now only call a function that executes the API calls and checks HTTP errors. If there are no errors, we&amp;rsquo;ll get the data back. If there are errors, an error is thrown. This way, the Redux code does not know anything about the details of the API call.&lt;/p>
&lt;p>Another reason to use a separate API package, is our desire to be able to set up an SDK more easily later on, when we start targeting another platform (think of an application, first only web, but where a mobile app will be added as well). It will then be possible to use shared code easily, without duplicate logic. Since these API packages become increasingly important with more dependent applications, the run-time guarantees also get more crucial. If the SDK says that a function returns a certain data type, we also want to guarantee this or else give an error message.&lt;/p>
&lt;h2 id="requirements-and-research">Requirements and research&lt;/h2>
&lt;p>Our research question consisted of several parts:&lt;/p>
&lt;ul>
&lt;li>How can we generically check a JSON object against a Typescript interface, without the need for duplicate code for type definitions?&lt;/li>
&lt;li>How can we achieve the above without having to modify other production code outside the API package to make this check possible?&lt;/li>
&lt;li>How can this be done in NodeJS, React Native and in the browser?&lt;/li>
&lt;/ul>
&lt;p>There are enough libraries that make it possible to check a JSON structure, based on a DSL (domain specific language). However, this wasn&amp;rsquo;t what we were looking for, because we were already using Typescript and didn&amp;rsquo;t want to maintain the same type definition in multiple ways. We would prefer to develop a solution where no code generation or extra step in compilation is needed, but everything on-the-fly during runtime (like Go).&lt;/p>
&lt;p>A few months ago, I read a &lt;a
href="https://blog.picnic.nl/guarding-a-react-native-application-from-evil-json-6f7cbb4404de"
target="_blank" rel="noopener"
>blog post by Picnic&lt;/a
>
, describing their project &amp;ldquo;Aegis&amp;rdquo;, in which they had offered a solution to this problem. However, I noticed that it was still difficult to implement. The code is open source, but there was no example of implementation on a larger scale, because this was done within Picnic&amp;rsquo;s proprietary app. A code generation step was also needed. We would, if possible, prefer not to have this extra step.&lt;/p>
&lt;p>On the Subreddit of Typescript I had placed a &lt;a
href="https://www.reddit.com/r/typescript/comments/i8yk6i/validating_objects_type_at_runtime/"
target="_blank" rel="noopener"
>post&lt;/a
>
, where I submitted my question. I primarily received responses with examples of code generation solutions. Some responses discussed runtime solutions, but these solutions were unnecessarily complex and/or required modifications within the build configuration of Typescript. We prefer to avoid this, because we prefer to keep something experimental separate from the rest of our production code, so that if we are not satisfied, we can revert the changes.&lt;/p>
&lt;p>A possible solution that popped into my head was the following:&lt;/p>
&lt;ul>
&lt;li>Load all type-definitions through the file system as strings&lt;/li>
&lt;li>Use the Typescript compiler as production dependency and parse these strings&lt;/li>
&lt;li>Compare the result of parsing against the JSON data to see if it matches the interfaces&lt;/li>
&lt;/ul>
&lt;p>However, this would mean that a substantial part of the Typescript compiler would have to become part of the app and thus increase the bundle size. The Typescript compiler is not the fastest in the world either, so this would take a considerable amount of extra time when it has to be done on-the-fly. In addition, the filesystem is only suitable for Node.js and not for browser environments, so compatibility could not be maintained. Unfortunately this solution was not feasible.&lt;/p>
&lt;p>Ultimately, I chose to use &lt;a
href="https://github.com/PicnicSupermarket/aegis"
target="_blank" rel="noopener"
>Picnic&amp;rsquo;s Aegis&lt;/a
>
, mainly because of the simplicity of the tool and because it can be used without modifying other aspects of the project (compilation steps, configurations or production code in other parts of the application).&lt;/p>
&lt;h2 id="implementation">Implementation&lt;/h2>
&lt;p>The final implementation is as follows. Within the API folder, all public types (that is, the arguments and return types of the entire API package) are defined in the &lt;code>types&lt;/code> folder. For all types in this folder, Aegis creates decoders and stores them in the &lt;code>internal&lt;/code> folder of the API package.&lt;/p>
&lt;p>In order to make the aforementioned solution usable for Bytecode, a few adjustments had to be made in Aegis. For example, we added ESLint comments at the top of the file. This was done in a &lt;a
href="https://github.com/lucianonooijen/aegis/tree/bytecode"
target="_blank" rel="noopener"
>fork of Aegis&lt;/a
>
on Github. This is the dependency used in Bytecode&amp;rsquo;s project. To build the decoders, a command has been added to the &lt;code>package.json&lt;/code> of the React-Native/Expo project. By simply running &lt;code>yarn run aegis&lt;/code>, Aegis is called with the right arguments and all decoders are built.&lt;/p>
&lt;p>The production code of the API package already used an internal &lt;code>returnOrThrow&lt;/code> function, which received an internal API response type (consisting of the response of the API and/or an error if it occurred), threw an error if it existed and otherwise returned the data. This function has been modified so that a second argument is given to the function, namely the decoder. In &lt;code>returnOrThrow&lt;/code> the decoder is then used to check the data before it is returned. See the example below:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-ts" data-lang="ts">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> { &lt;span style="color:#a6e22e">Decoder&lt;/span> } &lt;span style="color:#66d9ef">from&lt;/span> &lt;span style="color:#e6db74">&amp;#34;decoders/types&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">import&lt;/span> { &lt;span style="color:#a6e22e">guard&lt;/span> } &lt;span style="color:#66d9ef">from&lt;/span> &lt;span style="color:#e6db74">&amp;#34;decoders&amp;#34;&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">interface&lt;/span> &lt;span style="color:#a6e22e">APIResultSuccess&lt;/span>&amp;lt;&lt;span style="color:#f92672">T&lt;/span>&amp;gt; {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">data&lt;/span>: &lt;span style="color:#66d9ef">T&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">error?&lt;/span>: &lt;span style="color:#66d9ef">undefined&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">interface&lt;/span> &lt;span style="color:#a6e22e">APIResultFailure&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">data?&lt;/span>: &lt;span style="color:#66d9ef">undefined&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#a6e22e">error&lt;/span>: &lt;span style="color:#66d9ef">string&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>}
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">type&lt;/span> &lt;span style="color:#a6e22e">APIResult&lt;/span>&amp;lt;&lt;span style="color:#f92672">T&lt;/span>&amp;gt; &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">APIResultSuccess&lt;/span>&amp;lt;&lt;span style="color:#f92672">T&lt;/span>&amp;gt; &lt;span style="color:#f92672">|&lt;/span> &lt;span style="color:#a6e22e">APIResultFailure&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">throwOrReturn&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &amp;lt;&lt;span style="color:#f92672">T&lt;/span>&amp;gt;(&lt;span style="color:#a6e22e">result&lt;/span>: &lt;span style="color:#66d9ef">APIResult&lt;/span>&amp;lt;&lt;span style="color:#f92672">T&lt;/span>&amp;gt;, &lt;span style="color:#a6e22e">decoder&lt;/span>: &lt;span style="color:#66d9ef">Decoder&lt;/span>&amp;lt;&lt;span style="color:#f92672">T&lt;/span>&amp;gt;)&lt;span style="color:#f92672">:&lt;/span> &lt;span style="color:#a6e22e">T&lt;/span> &lt;span style="color:#f92672">=&amp;gt;&lt;/span> {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">if&lt;/span> (&lt;span style="color:#a6e22e">result&lt;/span>.&lt;span style="color:#a6e22e">error&lt;/span>) {
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">throw&lt;/span> &lt;span style="color:#66d9ef">new&lt;/span> Error(&lt;span style="color:#a6e22e">result&lt;/span>.&lt;span style="color:#a6e22e">error&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> }
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#75715e">// We can assume that data is valid (type T) if no error was found
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">data&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">result&lt;/span>.&lt;span style="color:#a6e22e">data&lt;/span> &lt;span style="color:#66d9ef">as&lt;/span> &lt;span style="color:#a6e22e">T&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">decodeChecker&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">guard&lt;/span>(&lt;span style="color:#a6e22e">decoder&lt;/span>);
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#66d9ef">const&lt;/span> &lt;span style="color:#a6e22e">_&lt;/span> &lt;span style="color:#f92672">=&lt;/span> &lt;span style="color:#a6e22e">decodeChecker&lt;/span>(&lt;span style="color:#a6e22e">data&lt;/span>); &lt;span style="color:#75715e">// Throws if it&amp;#39;s not valid
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span> &lt;span style="color:#66d9ef">return&lt;/span> &lt;span style="color:#a6e22e">data&lt;/span>;
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>};
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">export&lt;/span> &lt;span style="color:#66d9ef">default&lt;/span> &lt;span style="color:#a6e22e">throwOrReturn&lt;/span>;
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If the return body does not satisfy the decoder, an error is thrown, which can be caught when calling the API call.&lt;/p>
&lt;h2 id="wishlist">Wishlist&lt;/h2>
&lt;p>For now, the implementation of JSON type checks is still experimental. Within the codebase, this type check only affects a small (separate) part, so it can be easily removed later on. This is why it is not automated yet, something we would like to add later on. Automating this without modifying the build configuration can still be a challenge. Until we automate this completely, we can add a check in the CI pipeline that gives an error message when running Aegis causes file changes in Git, meaning the decoders have not been updated after making changes to the type definitions.&lt;/p>
&lt;p>Another very good use-case of this setup would be the end-to-end testing of the API for which the package is built. The end-to-end testing of APIs is something that has been on the Bytecode wishlist for some time now. JSON type checking can also provide great added value, allowing you to immediately check whether the API returns data according to the expectations.&lt;/p>
&lt;p>Regarding improvement to Aegis itself, at the moment Aegis is mainly built for the &amp;ldquo;happy flow&amp;rdquo;, there are still some edge cases that don&amp;rsquo;t quite work. The tool is now definitely usable, but improvements are still needed for large-scale use.&lt;/p>
&lt;p>Support for Aegis configuration files is another useful addition. Now arguments for &lt;code>importPath&lt;/code> and &lt;code>outputFile&lt;/code> must be given as CLI options. Simply calling &lt;code>aegis generate&lt;/code>, which then loads the configuration by itself would be a nice addition.&lt;/p>
&lt;p>If it turns out that this workflow works very well for Bytecode, there is a good chance that we will further develop the Aegis tool ourselves, open-source of course. For now, the tool is not yet available on NPM. That would be the first step towards a stable release.&lt;/p>
&lt;h2 id="example-project">Example project&lt;/h2>
&lt;p>As an addition to this article, an example project is also available, with a simple API call, where the tool can be seen in action.&lt;/p>
&lt;p>Check out the &lt;a
href="https://github.com/lucianonooijen/ts-runtime-json-checks-example"
target="_blank" rel="noopener"
>project on Github&lt;/a
>
.&lt;/p></description></item><item><title>How the corona pandemic might permanently change our work-attitude</title><link>https://lucianonooijen.com/blog/lasting-effects-corona-pandemic-on-work-attitude/</link><pubDate>Tue, 28 Jul 2020 00:00:00 +0300</pubDate><guid>https://lucianonooijen.com/blog/lasting-effects-corona-pandemic-on-work-attitude/</guid><description>&lt;p>&lt;em>This article is a translated and slightly modified version of the article I originally wrote in Dutch for Bytecode. To read the original, click &lt;a
href="https://web.archive.org/web/20210303031257/https://bytecode.nl/insights/coronacrisis-positief-effect/"
target="_blank" rel="noopener"
>here&lt;/a
>
&lt;/em>.&lt;/p>
&lt;p>Whether we like it or not, the corona pandemic is far from over. Even though quite some countries are slowly going back to &amp;ldquo;normal&amp;rdquo; (albeit with some more distance between each other), others have yet to reach the peak of their first wave.&lt;/p>
&lt;p>We should not forget that, for most people, it will take a while to go back to the large office buildings, nearly hugging each other in the elevator while going to the meeting with everyone in person, cramming 8 people into a tiny room. Should we actually go back to this?&lt;/p>
&lt;h2 id="working-before-corona-times">Working before corona times&lt;/h2>
&lt;p>If you think about it, the current mindset about work is pretty crazy. Why do I have to be in the office to do work that I might be able to do a lot more efficiently in other places? Why do I still have to go to the lecture hall, where I can&amp;rsquo;t skip parts I already know and where I can&amp;rsquo;t rewind if I want to hear something again (without delaying the lecture for others)?&lt;/p>
&lt;p>Before the corona crisis, few people were lucky enough to have the opportunity to decide their own approach to work. Mostly, remote work is only allowed when ill. Starting earlier in the morning, so that you can finish earlier in the day? No chance of success. Starting later and finishing later because you&amp;rsquo;re not a morning person? Certainly not.&lt;/p>
&lt;p>&lt;img src="https://lucianonooijen.com/img/0001-1.jpg" alt="lecture hall">&lt;/p>
&lt;h2 id="our-mindset-about-working-can-be-improved">Our mindset about working can be improved&lt;/h2>
&lt;p>Is it really necessary to work in this &amp;ldquo;pre-corona&amp;rdquo; way? Do you have to be in the office for everything? Of course not everyone can (or wants to) be a &lt;a
href="https://en.wikipedia.org/wiki/Digital_nomad"
target="_blank" rel="noopener"
>digital nomad&lt;/a
>
, flying across the world to work online in hotels. However, there are possibilities to make work more enjoyable for everyone. I don&amp;rsquo;t envision a shelf filler, car mechanic or electrician who works from home at times that suit him. But a translator, consultant or marketeer?&lt;/p>
&lt;p>Personally, I am very attracted to the ideas from the &lt;a
href="http://asyncmanifesto.org/"
target="_blank" rel="noopener"
>async manifesto&lt;/a
>
, a set of insights on how software development can be better implemented. The ideas come down to this: use modern tools, create a flexible work environment, do not disturb people&amp;rsquo;s concentration unless it&amp;rsquo;s urgent and only hold meetings when it&amp;rsquo;s really necessary. The async manifesto is focused primarily on software developers, but I think anyone with a non-physical profession can benefit from the tips given here.&lt;/p>
&lt;p>When it comes to productivity, it seems that working from home (in a quiet office) is a lot &lt;a
href="https://www.inc.com/marcel-schwantes/new-study-reveals-why-working-from-home-makes-workers-more-productive.html"
target="_blank" rel="noopener"
>more productive&lt;/a
>
than working at the office. It is fairly easy to understand why this is the case for many people. Every time you are interrupted in a &amp;ldquo;&lt;a
href="https://www.nrc.nl/nieuws/2016/03/30/de-superkracht-van-de-21ste-eeuw-1603307-a382406"
target="_blank" rel="noopener"
>deep-work&lt;/a
>
&amp;rdquo; session, it takes &lt;a
href="http://blog.idonethis.com/distractions-at-work/"
target="_blank" rel="noopener"
>25 minutes&lt;/a
>
to get your focus back. Not to mention the &amp;ldquo;this-could-have-been-an-email&amp;rdquo;-meetings that take an hour each.&lt;/p>
&lt;p>How great would it be, instead of being constantly interrupted for some trivial question, to just answer non-urgent questions a few times per day? To not be disturbed by people who talk too loudly about yesterday&amp;rsquo;s soccer game? As Jason Fried explains in his &lt;a
href="https://www.ted.com/talks/jason_fried_why_work_doesn_t_happen_at_work"
target="_blank" rel="noopener"
>TED talk&lt;/a
>
, you have to be able to work long uninterrupted stretches to really get something done.&lt;/p>
&lt;p>&lt;img src="https://lucianonooijen.com/img/0001-2.jpg" alt="cat on keyboard tired of hearing corporate office jargon">&lt;/p>
&lt;h2 id="the-corona-pandemic-offers-an-opportunity-to-reflect-on-the-current-way-of-thinking">The corona pandemic offers an opportunity to reflect on the current way of thinking&lt;/h2>
&lt;p>Now that a large part of us is still working from home due to the corona crisis, we have a chance to see what really works for us. Is that 9-to-5 job really ideal, or does a 10-to-6 or 7-to-3 job fit better, perhaps? Or why not 7-to-4 with an hour&amp;rsquo;s break to run a lap? And if 9-to-5 office-based work turns out to suit you best, that&amp;rsquo;s fine too, of course!&lt;/p>
&lt;p>How am I actually most productive? And more importantly, how do I actually get the most satisfaction from my work? These are questions to which we will be able to find an answer in the near future, because we now have room for experimentation.&lt;/p>
&lt;p>Hopefully the changes in (higher) education will also have a lasting effect, because of the long-awaited technological innovation.&lt;/p>
&lt;p>I&amp;rsquo;m not in favour of closing down all the offices and changing to remote work as a standard, which is naturally unfeasible. For example, I also think that face-to-face meetings work better than talking on the phone. But I believe that - with companies large and small - a lot can be improved compared to the current situation, if the right balance can be found. Let&amp;rsquo;s hope that this will be a positive lasting effect of this corona crisis.&lt;/p></description></item><item><title>Hello, world!</title><link>https://lucianonooijen.com/blog/hello-world/</link><pubDate>Sat, 18 Jul 2020 00:00:00 +0300</pubDate><guid>https://lucianonooijen.com/blog/hello-world/</guid><description>&lt;h2 id="welcome">Welcome&lt;/h2>
&lt;p>Big thanks for checking out my personal blog!&lt;/p>
&lt;p>As of today, my website is completely in English and I&amp;rsquo;ve also added &lt;a
href="https://lucianonooijen.com/learning"
>my learning page&lt;/a
>
on here, where I tell about my learning path to try and become a self-taught computer scientist.&lt;/p>
&lt;p>With this, I&amp;rsquo;ve also added this blog on my website, where I&amp;rsquo;ll be posting from time to time about my experiences in learning, or to share information I hope will be valuable to others.&lt;/p>
&lt;p>I&amp;rsquo;m also thinking about giving my site an overhaul, to improve the styling, readability and responsiveness and to add some features, but until this website is booming with traffic, I think this site will be more than just fine.&lt;/p>
&lt;p>Anyway, that&amp;rsquo;s it for now!&lt;/p></description></item></channel></rss>