Infer types for handlers in separate files for ElysiaJS

1.8k Views Asked by At

I am using ElysiaJS to create an API. The code is in an open-source repo here.

I have three files auth.routes.ts, auth.handlers.ts and auth.dto.ts. The routes files has the path, the validation object and the handler function. The validator object is exported from the auth.dto.ts (data transfer object). The handler function which has all the logic and queries is exported from the auth.handler.ts.

auth.routes.ts

const authRoutes = new Elysia({ prefix: '/auth' })
  /** Injecting Database Connection */
  .use(databaseConfig)
  /** Register User with Email and Password */
  .post('/register', registerWithEmailAndPassword, registerWithEmailAndPasswordDTO)

export { authRoutes }

Here the databaseConfig is an Elysia Plugin which has the connection code for the database.

auth.dto.ts

import { t } from "elysia"

export const registerWithEmailAndPasswordDTO = {
  body: t.Object({
    email: t.String({ format: 'email' }),
    password: t.String({ minLength: 6 }),
  })
}

auth.handlers.ts

/** Importing Schema */
import { type Context } from "elysia"

import Schema from "../../schema"
import { hashPassword } from "../../utils/auth.utils"

/** Destructuring Schema */
const { users } = Schema

export const registerWithEmailAndPassword = async (context: Context): Promise<string>  => {
  const { set, body: { email, password }, db } = context

  // more code here
}

If the handler is placed in the 'auth.routes.ts' all relevant types are injected by Elysia and everything works as expected but if the handler function is present in a separate file, adding the Context to the type of the argument does not work. It does not add types for the body params injected by validator object and the database config object which is added as a plugin.

How can I infer these types in my handler function in a separate file?

Edit

The type for the argument for the handler works if I construct the type manually. The code for it is below:

import { dbType } from "../../config/database.config"
type RegisterType = Context & {
  body: {
    email: string
    password: string
  },
db: () => dbType
}

the registerType will be used in the handler as export const registerWithEmailAndPassword = async (context: RegisterType).

database.config.ts

// create the connection
const connection = connect({
  host: process.env.DB_HOST,
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD
})

const db = drizzle(connection, { schema, logger })
const databaseConfig = new Elysia({ name: "databaseConfig" })
  .decorate('db', () => db)

export type dbType = typeof db
export default databaseConfig

Edit 2

I have already done everything that's mentioned in the Dependency Injection docs but it is still not working.

2

There are 2 best solutions below

0
Dale Ryan On

Elysia cannot type external route handlers

I have done the same pattern as you did, if the route Handlers are separate from the actual route definition, Elysia has no way of propagating the types to the handlers. A simple code example for demonstration:

// src/routes/index.ts

import { Elysia } from 'elysia'
import databaseConfig from 'src/lib/database'
import { testHandler } from 'src/routes/handlers'
import { testBodySchema } from 'src/schema'

const routes = new Elysia()
  .use(databaseConfig)
  .post('/test', testHandler, {
    body: testBodySchema
  })

export default routes

And in our handlers index.ts file, Elysia won't be able to pass the types from routes file:

// src/routes/handlers/index.ts

export const testHandler = async ({ body, database }) => {
  // body here will not be typed (unless manually giving it types)
  // database also will not be typed
}

How to properly initialize routes externally with Elysia without losing types?

Good thing there is a much more Elysia-esque way of doing this, Elysia was made for this purpose.

Going back to our routes index.ts file, I made some changes, it shall no longer hold the actual definition of routes, instead we pass the route definition from elsewhere:

// src/routes/index.ts

import { Elysia } from 'elysia'
import authRoutes from 'src/routes/auth'
import anotherRouteGroup from 'src/routes/another'

const routes = new Elysia()
  .use(authRoutes)
  .use(anotherRouteGroup)

export default routes

We then move all auth related definitions inside auth index.ts file, we then define another Elysia instance for our auth routes (yes this is the Elysia way of defining routes properly):

Also, creating new 10,000 Elysia instance took ~8ms so mostly don't affect performance.

saltyaom Qoute from discord chat

enter image description here

With that said let's continue with auth routes. Defining route handlers inline will make Elysia do its thing and propagate types for us!:

// src/routes/auth/index.ts
import { Elysia } from 'elysia'
import databaseConfig from 'src/lib/database'
import { loginBodySchema } from 'src/schema'

const routes = new Elysia({ prefix: '/auth' })
  .use(databaseConfig)
  .post('/login', ({ body, database }) => {
    // body is now typed!
    // database is also typed!
  }, {
    body: loginBodySchema
  })
  .post('/register', () => {})

export default routes
0
Nate Mansfield On

Consider the following:

// auth.dto.ts
const registerWithEmailAndPasswordDTO = t.Object({
    email: t.String({ format: 'email' }),
    password: t.String({ minLength: 6 }),
})

export type RegisterWithEmailAndPasswordDTO = Static<typeof registerWithEmailAndPasswordDTO>

export const authModel = new Elysia({ name: 'authModel' })
    .model({
        registerWithEmailAndPasswordDTO,
    })


// auth.service.ts
export const authService = new Elysia({ name: 'authService' })
    .derive(({ set }) => ({
        registerWithEmailAndPassword: async (dto: RegisterWithEmailAndPasswordDTO) => {
            // register user here

            // can use set object here
            set.status = 500;
        }
    }))

// auth.controller.ts
export const authController = new Elysia({ prefix: '/auth' })
    .use(authModel)
    .use(authService)
    .post("/register", ({ registerWithEmailAndPassword, body }) => registerWithEmailAndPassword(body),
        { body: "registerWithEmailAndPasswordDTO" })

Using the .use method, you are able to access the contents of one plugin from another in a typesafe way:

  • By defining models using .model in a plugin, schemas can be shared by use-ing that plugin
  • By defining context-aware functions using .derive, these functions can also be shared the same way without sharing any state across multiple requests

Also, consider jumping in the Elysia discord for much quicker support from other users