Skip to content

NextVMA Next-Generation FiveM Framework

TypeScript-first. Type-safe. PLA-compliant. Built for modern RP servers.

Quick Look โ€‹

Build a banking module in three files:

typescript
// modules/banking/src/server/service.ts
export class BankingService {
  private balances = new Map<number, { cash: number; bank: number }>()

  async transfer(from: number, to: number, amount: number) {
    const a = this.balances.get(from) ?? { cash: 0, bank: 0 }
    if (a.cash < amount) throw new Error('INSUFFICIENT_FUNDS')
    a.cash -= amount
    const b = this.balances.get(to) ?? { cash: 0, bank: 0 }
    b.cash += amount
    this.balances.set(from, a)
    this.balances.set(to, b)
  }
}
typescript
// modules/banking/src/server/router.ts
import { defineRouter, procedure, RpcError, z } from '@nextvm/core'

export function buildBankingRouter(service: BankingService) {
  return defineRouter({
    transfer: procedure
      .input(z.object({ to: z.number(), amount: z.number().positive() }))
      .mutation(async ({ input, ctx }) => {
        if (!ctx.charId) throw new RpcError('NOT_FOUND', 'No active character')
        await service.transfer(ctx.charId, input.to, input.amount)
        return { ok: true }
      }),
  })
}
typescript
// modules/banking/src/index.ts
import { defineExports, defineModule, z } from '@nextvm/core'
import { buildBankingRouter } from './server/router'
import { BankingService } from './server/service'

export type BankingExports = ReturnType<typeof buildExports>
const buildExports = (s: BankingService) => defineExports({
  service: s,
  transfer: s.transfer.bind(s),
})

export default defineModule({
  name: 'banking',
  version: '0.1.0',
  config: z.object({}),

  server: (ctx) => {
    const service = new BankingService()
    const router = buildBankingRouter(service)
    ctx.setExports(buildExports(service))
    ctx.log.info('banking ready', { procedures: Object.keys(router).length })
  },

  client: () => {},
})

Then nextvm build and you're done. The framework generates fxmanifest.lua, bundles your locales, splits server + client, validates your config schema, and produces a FXServer-ready resource under dist/.

Why NextVM? โ€‹

ConcernESX/QBCoreNextVM
Type safetyNoneFull compile-time
Event registrationManual RegisterNetEventGenerated by defineRouter
Input validationManual or skippedAutomatic via Zod
Rate limitingManual or noneBuilt-in per-player
fxmanifest.luaHand-writtenAuto-generated
Config validationNoneZod at startup
NUI communicationManual message passingTyped ctx.ui.send()
LocalizationManual Lua tablesBuilt-in i18n with t()

Read the full comparison โ†’

Released under the LGPL-3.0 License.