Tom Gobich

@tomgobich

Developer & dog lover. I teach AdonisJS, a full-featured Node.js framework, at Adocasts where I publish weekly lessons. Professionally, I work with JavaScript, .NET, and SQL Server.

Adocasts

Burlington, KY

Learning initiated
178
Lessons started
Knowledge gained
79
Lessons completed
Total learning time
13h 4m
Spent learning
Community impact
528
Comments & forum posts

Recent Activity

Here's what @tomgobich has been up to this past year

  • Replied to Thank you for your work @tomgobich

    Thank you for watching, @edikurniawan-dev!!

  • Replied to Thank you so much for your work !

    Thank you @himito!

  • Published news Saying Goodbye to Adocasts Plus

  • Published news What's New in AdonisJS - Week of May 31, 2026

  • Published lesson Understanding by Doing, Clean Slate

  • Published lesson Sign Up & Securing User Passwords

  • Published lesson Sign In & Sign Out Flows

  • Published lesson Using the Authenticated User

  • Published news What's New In AdonisJS - Week of May 24, 2026

  • Published lesson Query Builder Basics

  • Published lesson Working with Model Relationships

  • Published lesson Pagination

  • Replied to Hey Tom, thanks for this new series, it's helping a lot !I am...

    Hey @cgregoire! Glad to hear it's helping!

    I wouldn't really call that an issue, per say, but you're correct. Like many things, just because it's a solution doesn't mean it's a good solution for all use-cases. If your application is going to need a combination of UUID, int, etc IDs then a blanket matcher for all IDs isn't a good option. If, however, your IDs are all normalized to one type, like integer values, then it will fit the bill just fine!

    Good call on the ul, that could definitely go within the else to prevent the empty ul from being added!

    Thank you for taking the time to provide feedback!! I appreciate it!

  • Published lesson CRUD Basics

  • Published lesson Factories & Seeders

  • Published lesson Database Migrations

  • Published lesson Models, Schema Classes, & Relationships

  • Published lesson Middleware & Grouping Routes

  • Published lesson Sessions & Flashing Messages

  • Published lesson Components, Layouts, & Partials

  • Published lesson Form Basics, Method Spoofing, & CSRF

  • Published lesson Validation & Flash Storage

  • Published lesson Route Names & Type-Safe Route Generation

  • Replied to So is the "Forever" plan being phased out completely or will...

    My plan, at this time, is to bring it back as an option once I'm done with my break.

  • Replied to discussion Default front-end stack for the default adonisjs project

    Both the now outdated Let's Learn AdonisJS 6 and the in-progress/updated Let's Learn AdonisJS 7 series focus on EdgeJS instead of React or Vue. By using EdgeJS, we can focus fully on what AdonisJS has to offer and not alienate folks who prefer either Vue or React, as this series is meant to be an introduction to AdonisJS and not Vue or React.

    EdgeJS is a server-side template engine like Blade is for Laravel and Razor is for .NET. With AdonisJS you can easily use React or Vue either via Inertia, an in-app SPA, or a separate decoupled application.

  • Published lesson Route Parameters & Matchers

  • Published lesson Controllers, Barrel Files, & Subpath Imports

  • Replied to Ah, 7 is out! How hard is it to upgrade our v6 project to 7?...

    Yep, sure is! 🙂 Should be relatively quick and easy! The core team has even provided AI agent instructions, if that's your sort of thing. You can find that along with all the upgrade instructions here: https://docs.adonisjs.com/v6-to-v7.

    I don't have any upgrade instructions of my own nor have I upgraded any of my packages at this time, unfortunately. I've been putting all my resources into making this series as good as I can get it!

  • Published blog post The Road Ahead & A Brief Break

  • Published lesson Defining Routes & Rendering Views

  • Published lesson View State & Passing Data to Views

  • Published lesson EdgeJS View & Tag Syntax

  • Replied to Oh yes, I was actually mixing up encryption with signedUrlFor...

    So, you should reach for a database stored token over a Signed URL when:

    • You need instant revocation. If you're granting access to an operation one-time and that limitation is important, then Signed URLs aren't a good option. Might be single-use access to a file or something like password resets.

    • You need to track usage. Like limiting how many team invites an organization can send

    • You need long lived access. If you're granting access to something for a month, for example, having that in the db would be a safer bet as time increases the likelihood something will need to change.

    If those three things don't apply, chances are a Signed URL will work just fine. The benefit of Signed URLs is that they're easier and quicker to implement and scale much better for heavy traffic URLs since they don't need to hit the database. If you have the time and resources and you're unsure which to use, there's nothing wrong with playing it safe and using db-driven tokens.

    I appreciate the offer, but at the moment I'm currently only working on the Let's Learn AdonisJS 7 series. I'm planning on taking a brief sabbatical once that's completed. I'll have a blog post soon going into more detail 🙂

  • Replied to I recently discovered AdonisJS, and it immediately caught my...

    That's awesome to hear, @ricardo-fortis! I hope you enjoy AdonisJS and find these lessons helpful. If you have any questions are you go through them, always feel free to ask. Happy to help!

  • Replied to I'm glad I waited till v7 to learn adonis, I've been following...

    Yeah, definitely sounds like you're going to really enjoy the improvements v7 brings with it then! :D

    My pleasure! Thank you for watching, and I hope you enjoy @julius-emmanuel-oyigocho!!

  • Published lesson Introducing AdonisJS

  • Published lesson Dev Environment & Text Editor

  • Published lesson Lifecycle & Project Structure Tour

  • Published lesson Meet the Ace CLI

  • Published lesson Environment Variables

  • Published lesson Creating A New AdonisJS 7 Project

  • Replied to I’ve been wondering about something for a while: Why use Adonis...

    Hi @gregory! By encryption tokens, are you referring to Signed URLs? Where you generate a cryptographic signature that's attached to a URL you're sending.

    If so, you're spot on with your final point. The purpose of Signed URLs is merely to prove the request is authentic and hasn't been tampered with. The only power you have to revoke a Signed URL is an expiry that's set when the URL is generated. If revocation is a requirement, that's where you'd want to use something like a database stored token tied to the user.

    If, instead, you're talking about the actual encryption.encrypt() and encryption.decrypt() methods those actually do utilize node:crypto under the hood. For example, the AES-256-GCM driver for AdonisJS 7 uses createCipheriv, createDecipheriv, and randomBytes. This was similar in AdonisJS 6 as well.

    Additionally, string.generateRandom(), used in this lesson to generate the database stored password reset token also uses node:crypto's randomBytes method to create a cryptographically secure random string. That method originates from Poppinss.

    Hope that helps! If I misunderstood, please do let me know!!

  • Replied to Glad you find it helpful! Here is a Reddit topic with some...

    Thanks a bunch for sharing!! I'll give it a read through!! 🙂

  • Replied to Thanks! I ended up doing this before reading your post. I’m ...

    Anytime!! The solution you arrived at looks great! Since you're within an action, you could pluck that section out into a private method to separate concerns a bit and give that section of code a name. Completely optional though, either way works just fine!

    @inject()
    export default class SendPasswordResetEmail {
      constructor(protected rateLimiterService: RateLimiterService) {}
    
      async handle({ payload }: Params): Promise<SendPasswordResetResult> {
        // ...
    
        try {
          await ipLimiter.consume(ipKey)
          await emailLimiter.consume(emailKey)
    
          const result = await this.#rotateResetTokens(payload.email)
    
          if (result) {
            PasswordResetRequested.dispatch(result.user, result.token)
          }
    
          return { status: 'sent' }
        } catch (error) {
          if (error instanceof errors.E_TOO_MANY_REQUESTS) {
            return { status: 'rate_limited' }
          }
    
          throw error
        }
      }
    
      async #rotateResetTokens(email: string) {
        return db.transaction(async (trx) => {
          const user = await User.query({ client: trx }).where('email', payload.email).first()
          const { token, hash } = generateTokenAndHash()
    
          if (!user) return null
    
          await RevokePasswordResetTokens.handle({ user })
          await CreatePasswordResetToken.handle({ user, hash })
    
          return { user, token }
        })
      }
    }
    Copied!

    Oh, please don't feel silly for missing it! I don't mind helping in the slightest, I like to link to the docs and even internal code when I know of it just to give more sources and context. I know having additional examples always helps me, and it might help someone else who stumbles across these comments at a later time as well.

    My pleasure, @gregory!! Anytime! 🙂

  • Replied to Hi @tomgobich I’m having trouble handling transactions after...

    Hi @gregory! I've got good news, transactions cascade through relationships. It seems like everything here is stemming from your user, so you should just need to attach it to your user and Lucid will do the rest!

    await db.transaction(async (trx) => {
      user.useTransaction(trx)
    
      await RevokePasswordResetTokens.handle({ user })
      await CreatePasswordResetToken.handle({ user, hash })
    })
    Copied!

    It looks like you're good here with what you have, but just as an additional note of something to be mindful of. Managed transactions rely on the exception being thrown to know whether something failed. So when using transactions across methods/actions like this be sure to be mindful about try/catch statements you may add down the road.

  • Replied to Adonis seems great so far. Adocasts is also quite great. They...

    Thanks a bunch for taking the time to right out this helpful and thoughtful constructive feedback, @yavor-kirov!! I greatly appreciate it, and it comes at a great time as I'm in the process of preparing the next iteration of this series for AdonisJS 7, which is releasing soon.

    I'll definitely take this into consideration and focus more on why using AdonisJS is awesome and how it saves time and resources! 😃 Thanks again!!

  • Replied to Yeah other videos are working, although I haven't gone through...

    Very strange, I'm terribly sorry about the issues! Extensions can cause the strangest issues, but I would expect other videos to be having issues as well if that were the case.

    Switching this series from our old video provider to our home grown solution is on my to do list. I'll go through this introduction module today and get it switched over. That should fix up whatever is going on with this lesson! Apologies for the inconvenience!!

  • Replied to Video isnt showing, seems broken

    Hi Julius! Sorry to hear you're having troubles with the video. It seems to be working okay over here. After inspecting our logs, it looks like your request might've been missing the info required for our DRM. I've got a request for this video without any of the needed DRM info shortly before your comment came through. Are other videos in this series working for you? And, are you using a browser or any extensions that might alter our player in any way?

  • Published lesson The Browser Client

  • Published lesson Testing Browser Form Submissions & Validation Feedback

  • Published lesson Authentication in Browser Tests

  • Replied to Since releasedAt is nullable in the Movie model, the Edit action...

    Good catch, thanks for sharing @adun! Apologies I missed that!!

  • Replied to Thanks for the help. I think there were several things I had...

    Anytime! Awesome, I'm super happy to hear you were able to figure it out and get it up and running! Issues like these can be kind of hard to help with from afar.

    You should be able to get at your app's log to see the exact error by SSH'ing into your server and running pm2 list to find the id of your app. Then, you can do pm2 show <id> to see where its logs are being stored and use vim to read them.

  • Replied to I built and ran it locally and I get ERROR (7830): Failed to...

    Just gave this a go locally on a new Inertia app and I get "Failed to load url inertia/app/ssr.ts" when I have NODE_ENV=development. The Inertia entrypoint is different between development and production. In production mode it'll be ./build/ssr/ssr.js. Setting NODE_ENV=production should fix that up so its looking in the correct spot for the build.

    I'm not getting the rollup options error though, but that sounds like it might just be because the SSR file couldn't be found and I have a hunch changing the NODE_ENV should fix that up as well. If, for any reason it doesn't, please share your vite.config.ts file! There might be a hint in there.

  • Published lesson The Auth Plugin

  • Published lesson Testing Auth Protected Routes

  • Published lesson Testing Authorization with Bouncer

  • Published lesson Piecing It All Together

  • Replied to I will double check the node versions, but it seems ctx.auth...

    Yeah ctx.auth is coming through undefined when it should be the authenticator, even when a user isn't logged in. So, something is causing it to be misconfigured on production but not locally. A difference in Node version is one possibility. Double-check that the packages needed for authentication @adonisjs/auth, argon2, bcrypt, etc are all installed as dependencies and not dev dependencies, that's another possibility.

    Attempting to follow your deploy build pipeline locally should help pinpoint the issue, especially if you're able to replicate the issue locally because then you can step through it with the debugger or console logs to see what is having troubles.

    Oh - also make sure your NODE_ENV within your environment variables is set to production! I've seen that cause some weird issues before.

  • Replied to discussion Prod Deployment

    Hey @aaron-ford! What's the error message you're getting?

    If it works fine locally but not in production, then the answer probably lies in a difference between those two environments, like maybe a different NodeJS version. Try installing the same version you're using in production on your local machine. Then build the project for production locally and attempt to run it to see if you're able to.

  • Replied to Thanks for your help, everything makes sense now. Hopefully ...

    Awesome, I'm happy to hear that! Anytime at all, @gregory! 🙂

  • Replied to Hey @tomgobich ! I’ve been watching your videos and checking...

    Hey @gregory, great question!

    For most things, applying the limiter at the route level with an HTTP limiter will likely suffice. Moving the limiter into the controller, service, or action is ideal when you need to:

    • Do something specific when a user is rate limited.
      For example, maybe you want to gracefully handle the response if you don't want the user to lose form state.

    • Do something before applying the rate limiter.
      For example, you might use a rate limit to restrict invalid login attempts to prevent someone from trying to brute force the login. In order to know whether a user should be penalized you first need to know if the provided credentials were invalid.

    Since HTTP limiters are run before the controller, by moving the limiter into the controller flow you're granted more control over how and when the limiter is applied and also how the response is handled. If you don't need that added control, then using HTTP limiters at the route level is ideal for its simplicity.

    Anytime!! Hope this helps!

  • Published lesson Database Test Runner Hooks

  • Published lesson Testing Database-Driven Endpoints

  • Published lesson Testing with Lucid Model Factories

  • Published lesson Testing Model Logic

  • Anniversary milestone Thanks for spending 5 years with Adocasts!

  • Published lesson testing VineJS Validations & Working with CSRF

  • Published lesson Asserting JSON Structures

  • Published lesson Asserting OpenAPI Specifications

  • Published lesson Testing File Uploads

  • Replied to Hello @tomgobich Why not using .preload('user') instead of...

    Hi @gregory!

    Mostly just boils down to preference in syntax. Either solution here would work just fine and would result in the same number of queries as well! So, using preload in this case is a-okay! 🙂

  • Published lesson Functional vs Unit Testing

  • Published lesson Meet the API Client

  • Published lesson Testing GET Endpoints

  • Published lesson Testing Ace Console Commands

  • Replied to Hello @tomgobich Do you think it’s reasonable to build a booking...

    Hi @gregory! Yeah, using HTMX for this would definitely be a reasonable option! You could also look into Unpoly; it's like HTMX but takes care of more automatically for you.

    React is great if your app requires a lot of state management or interactivity. Things like dashboards, drag-and-drops, multi-step or complex if/else forms can be made easier with something like React because React can send requests to the server on an as-needed basis to persist or fetch data, all while maintaining the same state locally and dynamically updating the view with ease.

    HTMX or Unpoly, on the other hand, are only going to have access to the state that is sent on a per-request basis. They are great if your flows are going to be more linear, as that allows your requests, and therefore state, to go through the server.

    For example, say you're going on a trip and flying to get to your destination and back. Your roommate plans on staying home. The important things you'll need for your trip get packed into a luggage bag to go with you. Everything else you own stays home, with your roommate. You don't throw those things staying home away, however, because you will need them when you get back from your trip.

    In this example, your belongings are state, and your roommate is a different portion of the page that doesn't need updating (maybe you're the form and your roommate is the shell/layout of the page).

    React allows you to leave things at home via local state. Without React, everything must go with you (the request) or be thrown in the trash, meaning it won't be there when you get home (the response). You can, however, still use money you took with you to buy extra things to bring home (using IDs to query data from the database, for example).

    Without React and without HTMX/Unpoly, everything must go with you or else it'll be thrown to the trash, and your roommate is forced to go with you as well. So HTMX/Unpoly gives you the flexibility to dynamically update specific portions of your page from the server on an as-needed basis, but for those updated portions, they only have access to the info that was sent with the request. Typically, that's form data, query strings, or route parameters. In most cases, this ends up being perfect, and that extra local state management provided by React isn't needed.

    If you're using HTMX/Unpoly and there are one-off portions where you need more interactivity or state management, you can always reach for vanilla JavaScript or even a lightweight package, like AlpineJS, to add state and reactivity to your page. This is the exact setup I'm using for Adocasts (Adonis + Unpoly + Alpine). At the end of the day, whether to use HTMX/Unpoly or something like React is a scale. Each requirement can add weight to the scale, and eventually the heft may merit something like React. That could be something as small as familiarity and a tight timeline.

    If you are leaning towards React, there's an integration with AdonisJS to help blend the two together called InertiaJS that will help make it less daunting as well.

    If you're really early on in your journey, I'd recommend just making your app with AdonisJS first, using traditional form submissions. Then, sprinkling HTMX or Unpoly into it. That'll give you a foundation of familiarity before introducing a foreign object.

    Hope this helps! ~ sorry my answer got really long

  • Published lesson Testing a Simple Service

  • Published lesson Introduction to Mocks & Dependency Swapping

  • Published lesson Introduction to Fakes

  • Published lesson Unit Testing Event Listeners

  • Completed lesson Testing a Simple Service

  • Replied to Well done very helpful even for adonis 6. Mostly the same, extremely...

    Thank you for watching, @niemes! I'm happy to hear you found this helpful for AdonisJS 6 as well! 😀

  • Published lesson Testing Async Code & Exceptions

  • Published lesson Data-Driven Tests with Datasets

  • Published lesson Filtering & Controlling Test Runs

  • Replied to I spent the afternoon making it work with react. @inertiajs...

    Thank you for sharing, @gregory!! I'm happy to hear you were able to get it up and working with React!

  • Replied to Hello Tom, I would like to implement email verification, but...

    Hi @gregory! Great question! I would opt for redirecting to /email/verify/sent because if the user refreshes the page, with this being a URL of its own, they'll be returned right back to this message. If you were to just return inertia.render() this page, if the user refreshes, they'll be shown the registration form again since they aren't logged in within RegisterController.store. That might cause them to question if they actually registered, resulting in them attempting to register again.

  • Replied to The R2 way worked, did you face any issues with their egress...

    Hi @onesoni! Yeah, so far so good using Cloudflare R2 for our videos! I've yet to be charged for egress, so zero issues there. Only charge has been on data storage and that didn't start until we went over the 10GBs you get for free.

  • Published lesson Making Assertions

  • Published lesson Naming Tests

  • Published lesson Group & Test Lifecycle Hooks

  • Published lesson Why Bother Testing?

  • Published lesson The AdonisJS Testing Stack

  • Published lesson Our First Test

  • Published lesson Running Tests with the Test Runner

  • Completed lesson Why Bother Testing?

  • Replied to Hello! I just wanted to say how much I’ve enjoyed your videos...

    Thank you very much for the constructive feedback, @shark! There's nothing wrong or insecure with using callbacks for route handlers within small applications, but you're right they don't scale well at all. The app.makeUrl does also utilize join from node:path which normalizes to help protect against path traversal.

    My thinking here was to set a premise first, then replace them with AdonisJS implementations while also painting a picture on why you'd want to extract to service and controllers as things grow.

    My execution on that, however, did have room for improvement. I can definitely see your point, in that, it can feel disorienting/disappointing to start with one thing only to switch away from it a few lessons later. I have learned quite a bit from hindsight and feedback like yours on this series. If I were remaking this series today, it would look vastly different. It'd be more condensed, there are plenty of things I'd remove altogether (especially in this module). It'd also have more soft mentions to things like my sentence above mentioning app.makeUrl performs normalizations through node:path's join method.

    Thank you again for your feedback! I greatly appreciate you taking the time to share, and I hope you'll find I do it justice in future series!

  • Replied to Thanks for all these ideas! Before seeing that I added this...

    Anytime! Awesome, looks like what you had was a great alternative. I think the presenter approach will fit your use-case well! AdonisJS 7 will be introducing HTTP Transformers as an official solution to fill this space as well, which will be nice.

  • Published blog post Adocasts Plus Updates & Black Friday Sale

  • Replied to Hi Tom, Quick question concerning storage on a cloud provider...

    Hi @memsbdm! Good question, and you have a few options! You could use a model hook for this. If you're early on in your project architecture and you'd like to take this approach, I'd recommend maybe binding all your images via an Asset or Image model. That way, you can add the hook and the hook will only run when the user.asset is lazy/eager loaded.

    For example

    // the hook would run anytime the user is queried, which...
    // think of all the POST, PUT, DELETE requests that use your user
    const directUrl = user.avatarUrl
    
    // here the hook only runs when the asset is lazy/eager loaded
    // onto the user model, limiting the number of times the hook runs
    await user.load('asset')
    const relatedUrl = user.asset.url
    Copied!

    Alternatively, you could add a method a method to your DTO to populate the URL, something like:

    export default UserDto extends BaseModel {
      declare id: number
      declare username: string
      declare avatarUrl: string | null
    
      constructor(user: User) {
        this.id = user.id
        this.username = user.username
      }
    
      async fromModel(user: User) {
        const dto = new UserDto(user)
        
        dto.avatarUrl = await drive.use('s3').getUrl(user.avatar)
    
        return dto
      }
    }
    Copied!

    You could also switch from DTOs to a presenter style approach:

    export default class UserPresenter {
      async fromModel(user: User) {
        return {
          id: user.id,
          username: user.username,
          avatarUrl: await drive.use('s3').getUrl(user.avatar)
        }
      }
    }
    Copied!

    Lastly, if you're dealing with public images, you could set up your own endpoint to serve the image. Something like /imgs/:key and this endpoint can directly serve the image. Then, if your site is behind something like Cloudflare, you can set it up to cache /imgs/* to set it up as a sort of CDN endpoint. Your image src could then just be:

    <img src="/imgs/my-image-key.jpeg">
    Copied!

    Hope this helps!!

  • Replied to https://github.com/SortableJS/vue.draggable.next/issues/286 ...

    Glad you were able to find a workaround, and thanks for sharing!! That's the first I've seen of that one, I'll have to keep my eye on it.

  • Replied to Hello ! I was not sure if the presenters &amp; dtos could be...

    Awesome, glad to hear you got it working! Yeah, the number of files can definitely be a downside. You always have the presenter approach if you see that being an issue in your project since that approach allows you to cleanly define multiple shapes within a single class!

    As for the HTTP Transformer package, I believe right now it's available as part of the AdonisJS Insiders program and intended to release officially with AdonisJS 7. I don't know, however, if it has any restrictions limiting it to AdonisJS 7 projects. I think everything available via the insiders program is considered alpha at the moment, so it wouldn't be recommended to use in production at this time.

  • Replied to // handler.ts async handle(error: unknown, ctx: HttpContext...

    Hi Jaime! Not quite sure which repository this code comes from, but the exception handler there is merely changing how the E_TOO_MANY_REQUESTS exception is returned and displayed to the user. It won't change anything with the rate limiting behavior itself.

    The traditional web rate limit exception would print a blank page to the user with the exception message. This intercepts that behavior to instead display the exception message via a flash message.

    async handle(error: unknown, ctx: HttpContext) {
      // if the exception thrown is E_TOO_MANY_REQUESTS (rate limit)
      if (error instanceof errors.E_TOO_MANY_REQUESTS) {
        // flash form input data so it can be repopulated into any forms
        ctx.session.flashAll()
    
        // flash the rate limit exception so the user knows what happened
        ctx.session.flashErrors({
          E_TOO_MANY_REQUESTS: 'Too many login attempts. Please try again later.',
        })
    
        // redirect the user back to where they were
        return ctx.response.redirect().back()
      }
    
      // if the exception thrown wasn't E_TOO_MANY_REQUESTS, 
      // proceed as usual with the standard exception handling.
      return super.handle(error, ctx)
    }
    Copied!
    • app
    • exceptions
    • handler.ts

    Hope this helps!!

  • Replied to Yes that's it ! I was close to this approach, but I can't use...

    If I'm following correctly, you would either use the presenter approach or the DTO approach. They are two different solutions to do the same thing: change the shape of an object/class. So, assuming organization.borrowers is a relation to your User model…

    If you were doing DTOs, it look something like this:

    export default class UserBorrowerDto extends BaseDto {
      declare id: number
      declare firstname: string
      declare lastname: string
      declare email: string
      declare phone: string
    
      constructor(user: User) {
        this.id = user.id
        this.firstname = user.firstname
        this.lastname = user.lastname
        this.email = user.email
        this.phone = user.phone
      }
    }
    
    // app/dtos/user_borrower_profile.ts
    export default class UserBorrowerProfileDto extends BaseDto {
      declare id: number
      declare firstname: string
      declare lastname: string
      declare email: string
      declare phone: string
      declare address: string
    
      constructor(user: User) {
        this.id = user.id
        this.firstname = user.firstname
        this.lastname = user.lastname
        this.email = user.email
        this.phone = user.phone
        this.address = user.address
      }
    }
    
    // usage
    const borrowers = UserBorrowerDto.fromArray(organization.borrowers)
    const profiles = UserBorrowerProfileDto.fromArray(organization.borrowers)
    Copied!
    • app
    • dtos
    • user_borrower.ts

    If you're using presenters it'd look something like this:

    export type UserBorrowerPresenter = 
      ReturnType<typeof UserPresenter.borrower>
    
    export type UserBorrowerProfilePresenter = 
      ReturnType<typeof UserPresenter.borrowerProfile>
    
    export default UserPresenter {
      static borrower(user: User) {
        return {
          id: user.id,
          firstname: user.firstname,
          lastname: user.lastname,
          email: user.email,
          phone: user.phone
        }
      }
    
      static borrowerProfile(user: User) {
        return {
          id: user.id,
          firstname: user.firstname,
          lastname: user.lastname,
          email: user.email,
          phone: user.phone,
          address: user.address
        }
      }
    }
    
    // usage
    const borrowers = organization.borrowers.map((user) => {
      return UserPresenter.borrower(user)
    })
    
    const profiles = organization.borrowers.map((user) => {
      return UserPresenter.borrowerProfile(user)
    })
    Copied!
    • app
    • presenters
    • user.ts

    Hope that helps and I'm on the right track there!

    Additionally, static methods on classes are directly usable without instantiating an instance of the class. When static is omitted the method is defined on the prototype and is only usable on instances of the class, for example:

    class Caculator {
      // static method
      static add(a: number, b: number) {
        return a + b
      }
    
      // instance method
      subtract(a: number, b: number) {
        return a - b
      }
    }
    
    // not available on Caculator instances [new Calculator()]
    // because it's defined as a static method
    const addition = Calculator.add(5, 10) // 15
    
    // only available on Caculator instances
    // because it is not defined as a static method
    const calculator = new Calculator()
    const subtraction = calculator.subtract(10, 5) // 5
    Copied!
  • Upvoted comment What I wanted to explain :

  • Replied to What I wanted to explain :

    Ah - I see what you mean now, thank you! Unfortunately, being DRY isn't the best when it comes to DTOs and it's best to just be simple and create a separate DTO per property list you need. For example, you would have a separate DTO listing all their needed properties for:

    • UserPublicDto

    • UserPrivateDto

    • UserWithoutPasswordDto

    Now, typing here will get better in AdonisJS 7 as it'll introduce HTTP Transformers. What you have in your screenshot can be used now as well, though! You should be able to define a type using the object the method returns, for example something like:

    export type UserBorrowerPresenter = 
      ReturnType<typeof UserPresenter.borrower>
    
    export type UserBorrowerProfilePresenter = 
      ReturnType<typeof UserPresenter.borrowerProfile>
    
    export default UserPresenter {
      static borrower(user: User) {
        return {
          id: user.id,
          firstname: user.firstname,
          lastname: user.lastname,
          email: user.email,
          phone: user.phone
        }
      }
    
      static borrowerProfile(user: User) {
        return {
          id: user.id,
          firstname: user.firstname,
          lastname: user.lastname,
          email: user.email,
          phone: user.phone,
          address: user.address
        }
      }
    }
    Copied!

    I would still recommend explicitly defining the properties for each, though, for clarity sake! It'll help down the road, you'll be able to just glance at it to see exactly what it sends rather than having to piece it together from multiple different methods.

    Then, you can use the methods to serialize and the types to type on the client side.

    // serializing
    const borrower = UserPresenter.borrower(user)
    
    // type
    defineProps<{
      borrower: UserBorrowerPresenter
    }>()
    Copied!

    If I were recreating this series today, this is the approach I'd have taken over DTOs. DTOs are the go-to in C# which I also use heavily and that originally swayed my decision making. Either work just fine though.

    Hope this helps!!

  • Replied to Could you suggest a good way to manage hidden fields like user...

    Hi cgregoire! Not sure I fully follow what you're after, but I wouldn't advise including the password in any DTO being sent to the client. I'm working to update this lesson now to include a note about that, apologies I missed that in the DTO that was generated.

    If you're looking for a way to signal whether the user has a password, for example maybe you also have social authentication where the user may not have a password, then a flag in the DTO would be a sufficient solution! You could name that property however you'd like, but I would go with something like isPasswordSet or hasPassword.

    export default class UserDto extends BaseDto {
      // ...
    
      isPasswordSet: boolean
    
      constructor(user: User) {
        // ...
    
        this.isPasswordSet = !!user.password
      }
    }
    Copied!

    Hope this helps! If I missed the mark on what you were asking here, please do correct me!

  • Replied to Rather than have someone have to know the ids relations, how...

    The way I would recommend to do this is to implement slugs/aliases for those string values so that you have an extra step to ensure they're unique within the database via:

    • A unique constraint directly on the column in the database

    • A unique rule via the validator (if you allow it to be user-defined). You can also generate a unique slug for the user from the name.

    Without that unique constraint, by just using the name, you open up the possibility of Planned matching multiple id values within your database. You can also ensure slugs are all normalized and URL safe, lowercased_with_underscores for example, whereas your names might "Be A Lil' Complex" and need encoded for URL usage.

    Likelihood of that actually happening might be slim, but think of the use-case where someone might use color to differentiate between two equally named statuses. Planned with a red color could mean its planned with issues while Planned green means it's all done.

    Your route parameter, validator, and logic can then access it as slug and safely do a database lookup directly from it since the column would have a unique constraint on it. No need to worry about transforming it!

    Though, if you do need to get an id from the slug for any reason, I wouldn't worry about doing so in the validator. I would instead create a separate service/action method to perform that conversion and call that method from the controller. For example:

    async store({ response, params }: HttpContext) {
      const slug = params.slug
      const id = await GetStatusIdFromSlug.handle({ slug })
      const status = await StoreStatus.handle({ /* ... */ })
    }
    Copied!

    If we're talking accepting slugs in place of all id's needed to create a course, then you could create a separate action/method specifically for that, like StoreCourseFromSlugs or even StoreCourse.fromSlugs(). That could perform the conversion to the id then kick it along to the original store method.

    Hope that helps, Aaron!!

  • Replied to Can we clean up the error responses and not have it return the...

    Ah - it didn't occur to me, but I should've noted this in the lesson. Similar to the HTML Youch page, the frames here is only included in when debugging. It should be omitted when your application is running on production. I believe only message is included in production.

    If you do wish to customize the exception, though, you can do that within the exception's handle method. This can be done globally within the handle.ts method or any individual custom exceptions. You can also narrow in on a specific exception within handle.ts as well:

    /**
    * The method is used for handling errors and returning
    * response to the client
    */
    async handle(error: unknown, ctx: HttpContext) {
      if (error instanceof errors.E_HTTP_EXCEPTION) {
        return ctx.response.status(error.status).send({ message: 'Oh no!' })
      }
    
      return super.handle(error, ctx)
    }
    Copied!
  • Published lesson Generating Dynamic OG Images with AdonisJS & Puppeteer

  • Replied to Hi Tom,Thanks for the detailed response! Ah, that makes perfect...

    Anytime, Andrei!! Yep exactly, so long as you aren't using tiered caching, then your understanding is spot on! If you hit a Worker in the Madrid data center and that Worker performs a put into the cache, that item will be cached in Madrid. Tiered caching, however, will use origin servers, limiting the number of data centers it'll use to a select few. The Cache API doesn't support tiered caching though, so if you use that you'll be all set.

    Cache API is local to a data center, this means that cache.match does a lookup, cache.put stores a response, and cache.delete removes a stored response only in the cache of the data center that the Worker handling the request is in.

    I can't guarantee the Worker approach will be as performant as the global CDN, but so long as your Worker is lightweight, it should be fairly close! Also, unless you're an enterprise user, the cache file size limit is the same between Workers and the global CDN as well, 512MB.

  • Replied to discussion Cloudflare R2 for Video Storage

    Hi Andrei! Absolutely, happy to answer any questions you have!

    Costs

    To preface, I haven't yet transitioned our older Bunny Stream videos to Cloudflare R2 (still going to, just need to find the time). Additionally, Cloudflare R2 offers 10GB/mo of free R2 storage and I've only been running R2 for about 10 months now. So, I was under the free tier for a few months there. Also worth noting, Cloudflare won't bill you until your accrued charges are high enough to be worth a bill.

    So, I think I went over the free tier for R2 storage around March. Since then, I've only been billed twice for a total of $0.45. I'm still under their A & B operation free tier as well as the worker free tier and I'm not sure I'll ever go over those monthly amounts, so that 45 cents is purely from R2 storage.

    Bunny Stream on the other hand I was seeing a bit of a compounding effect on our charges. The more videos released meant more views. They also, at least at the time, have/had a $1/mo minimum charge. I exclusively used Bunny Stream for about 14 months starting under that $1/mo minimum charge and by the end of the 14 months it creeped up to around $5/mo… which is still super affordable compared to most other options. As I believe I mentioned in that blog post though, my fear was the compounding effect I was seeing due to egress.

    Worker Configuration, Video Authorization, & Caching

    First, the R2 bucket is private and can only be accessed via token. The worker serves as an access point to request files from the R2 bucket.

    The authorization on whether the user has permission to watch the video is done directly on the Adocasts site. When authorized, it bundles up an HMAC hashed message and passes it along via a header with the requests to the worker. The worker will then also bundle up an HMAC message for the requested video. If the messages match, then the user can watch. It essentially just ensures the request hasn't been tampered with and originated from the Adocasts site. I chose this approach because I didn't want to have to relay back to the Adocasts site from the worker to check for authorization.

    Here's how I'm building that message on the Adocasts side:

    const version = 'v1'
    const userId = user?.id ?? 'NA'
    const videoId = post.videoR2Id
    const expiration = DateTime.now().plus({ hours: 48 }).toISO()
    const payload = [version, userId, videoId, expiration].join('|')
    const signature = createHmac('sha256', env.get('R2_SIGNING_KEY'))
      .update(payload)
      .setEncoding('base64')
      .digest('hex')
    Copied!

    Due to the authorization being needed in the worker, requests between Adocasts and the worker aren't cached. However, requests between our worker and R2 bucket are. I don't track cache-hits so I'm not sure exactly how much its helping. That's one down side to using R2 instead of Bunny Stream, Bunny Stream does come with a bunch of out-of-the-box statistics.

    There's probably room for improvement, but here's specifically what I'm doing to cache… nothing special:

    const cacheKey = new Request(url.toString(), request)
    const cache = caches.default
    
    let response = await cache.match(cacheKey)
    
    // if cached, return directly
    if (response) {
      return response
    }
    
    // ... [fetching file]
    
    // add to cache
    ctx.waitUntil(cache.put(cacheKey, response.clone()))
    Copied!

    Hope this helps!!

  • Replied to How do you prevent hydration errors on the dates? The server...

    Yeah, good catch! The locale difference can definitely cause some issues. The hydration mismatch comes into play because what is rendered on the server differs from the expected render on the client. So, when Vue attempts to hydrate it notices this and warns about the markup being different.

    Use Server Time Until Mounted

    The best option is to use the server provided time until your Vue component has mounted. I believe something like this should work, using the onMounted hook to display the server provided value until Vue is all set up.

    <template>
      <div>
        The time is {{ displayDate }}
      </div>
    </template>
    <script setup>
    import { onMounted, ref, computed } from 'vue'
    import { DateTime } from 'luxon'
    
    const props = defineProps<{ date: string }>()
    const isMounted = ref(false)
    
    const displayDate = computed(() => {
      if (isMounted.value) {
        return DateTime.fromISO(props.date).toLocaleString()
      }
    
      return props.date
    })
    
    onMounted(() => (isMounted.value = true))
    </script>
    Copied!

    This could be extracted into a Vue component for easy reuse!

    Suppressed Warning (new in Vue 3.5+)

    A newer option is to suppress the hydration mismatch warning. Its best to try and fix the mismatch, but this can come in handy as a last resort.

    <template>
      <div data-allow-mismatch>
        The time is {{ displayDate }}
      </div>
    </template>
    Copied!

    Official Note from Vue Documentation

    The Vue documentation touches on this slightly, providing that below to provide a complete picture!

    The server and the client are in different time zones. Sometimes, we may want to convert a timestamp into the user's local time. However, the timezone during the server run and the timezone during the client run are not always the same, and we may not reliably know the user's timezone during the server run. In such cases, the local time conversion should also be performed as a client-only operation.

  • Replied to The reason it could use the same table is because a course is...

    Oh shoot - yeah, good catch, I completely forgot about there being a foreign key on that. Due to that foreign key, you would need a new table since the foreign key itself can only reference a single column inside of a single table. So, you'd need a table for your organization tokens and another for your course tokens, apologies I forgot about that.

  • Replied to So if we need multiple access tokens, like say we wanted to ...

    Yep, spot on Aaron! The access tokens are scoped by their type so by giving organizations one type and courses another you'll be able to use the same table for both. That's distinguish which tokens are specifically meant for organizations and which are meant for courses.

    If you want to use the same type for both, then I would recommend using separate tables. Otherwise, you could create a token for a course with an id of 3, and circumvent the system to use that same token for an organization with an id of 3, since there wouldn't be anything distinguishing the two from one another.

    Lastly, again spot on! You'll want to replicate the token configurations again for the course including the tokens guard, as you've got above! Then, when using the auth module, just utilize the use method to specify which guard you're targeting. That will give you the correct model type for the specified guard as well.

    // the use method accepts the key name given to the guard in config/auth
    const organization = auth.use('organization').user
    const course = auth.use('course').user
    Copied!
  • Replied to When using the action approach, where should we put common logic...

    If it is specific to a single resource, you can make it as an action within that resource's folder. For example, if you're using Ally, you might need to match the social user to a user in your database in a few spots. For this, we can make a GetUserFromAlly action. That tree might look like:

    app/
    └── actions/
        └── ally/
            ├── get_user_from_ally.ts
            ├── handle_ally_callback.ts
            └── handle_ally_redirect.ts

    If its specific to multiple resources or even no resources, you can make an action for it and place it within a general, common, or shared folder. For example, you might need to get a general location from an IP Address in a few places. For this, we can make a GetLocationFromIP action. That file tree might look like:

    app/
    └── actions/
        ├── auth/
        │   └── on_login_succeeded.ts (may notify users of new login location)
        ├── general/
        │   └── get_location_from_ip.ts
        └── users/
            └── get_user_sessions.ts (lists sessions & session location)
  • Replied to Very intersting

    Thanks asep-hermawan, hope you're enjoying!

  • Upvoted comment Very intersting

  • Published lesson Indexing Data as its Created

  • Published lesson Removing Indexed Documents when they're Deleted

  • Published lesson Performing a Multi-Search Across Indexes

  • Published lesson Performing Full-Text Search

  • Published lesson Making our Search Results Functional with Unpoly

  • Replied to I have been going through trying to clean up my console errors...

    Hey Aaron! Ah yes, sorry about that! That warning is in regards to Vue's fallthrough attributes, ie: values passed to the to the component that aren't declared as props. When the component has a single root node, Vue will default to just passing that undeclared prop as an attribute. However, when there are multiple root nodes, as I accidentally did with the Navigation.vue component Vue isn't sure what to do with them and issues that warning.

    I agree, just wrapping the component template contents in a div would be the preferred solution here!

    As for the pagination links, is this in regard to the next/prev links? If so, I think you should be able to do something like the below. Allow the href prop to be null, then use a v-if to determine if the link should be utilized. Or, you could use nullish coalesce when passing in the prop to default to an empty string. The next/prev links, coming directly from the paginator, will be null if there isn't a next or previous page to go to.

    So something like this using nullish coalesce:

    <PaginationPrev :href="lessons.meta.previousPageUrl ?? ''" />
    Copied!

    Or this inside the next/prev components:

    <script setup lang="ts">
    import { ChevronLeft } from 'lucide-vue-next'
    import { PaginationPrev, type PaginationPrevProps } from 'radix-vue'
    import { type HTMLAttributes, computed } from 'vue'
    import { Button } from '~/components/ui/button'
    import { cn } from '~/lib/utils'
    
    const props = withDefaults(
      defineProps<PaginationPrevProps & { 
        class?: HTMLAttributes['class']; 
        href: string | null
      }>(),
      {
        asChild: true,
      }
    )
    
    const delegatedProps = computed(() => {
      const { class: _, ...delegated } = props
    
      return delegated
    })
    </script>
    
    <template>
      <PaginationPrev v-bind="delegatedProps">
        <Button :class="cn('w-8 h-8 p-0', props.class)" variant="outline">
          <Link v-if="href" :href="href">
            <ChevronLeft class="w-4 h-4" />
          </Link>
          <template v-else>
            <ChevronLeft class="w-4 h-4" />
          </template>
        </Button>
      </PaginationPrev>
    </template>
    
    Copied!

  • Published lesson Creating A Meilisearch Service to Initialize Admin & Search Instances

  • Published lesson Creating a Command to Index Data in Meilisearch

  • Published lesson Creating our Database Migrations, Models, and Factories

  • Published lesson Seeding Fake Data to Index for our Multi-Search

  • Published lesson What We'll Be Building

  • Published lesson Meilisearch Installation & Setup

  • Published lesson Creating our AdonisJS Project & Setting Up our API Keys