Skip to content

Adding Actions

Actions are the entry point into your data layer. Create a separate action for each use case and invoke it from your framework (Pages/Layouts in Next.js, loaders in SvelteKit, routes in Express, etc).

Actions extend from Vla.Action and implement a handle() method:

import { Vla } from 'vla'
class ShowUserProfile extends Vla.Action {
async handle(params) {
const { id } = validateParams(params)
const db = new Database()
const user = await db.users.find({ id })
return {
name: user.name,
email: user.email,
joinedAt: user.createdAt
}
}
}

Invoke actions from your framework:

// Express
app.get('/users/:id', async (req, res) => {
const profile = await ShowUserProfile.invoke(req.params)
res.json(profile)
})
// Next.js
export default async function ProfilePage({ params }) {
const profile = await ShowUserProfile.invoke(params)
return <div>{profile.name}</div>
}
// SvelteKit
export const load = async ({ params }) => {
return {
profile: await ShowUserProfile.invoke(params)
}
}

In the next guides, we’ll refactor this example to use services and repos for better structure and reusability.

Actions are usually HTTP-aware. Use Context to access request data:

const AppContext = Vla.createContext<{
req: Request
res: Response
}>()
class RenderProfileView extends Vla.Action {
ctx = this.inject(AppContext)
async handle(userId: string) {
const db = new Database()
const user = await db.users.find({ id: userId })
this.ctx.res.render('profile', { user, isAdmin })
}
}
  • Actions aren’t meant to be reusable. There’s usually just one place that invokes a specific action
  • Actions should return data optimized for their use case (e.g., the frontend). The frontend shouldn’t need to process data further
  • Actions should not invoke other actions
  • Actions can directly query the database, though using repos or services is recommended for better structure

To reuse business logic across actions, use Services.