install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/TerminalSkills/skills/adonisjs" ~/.claude/skills/comeonoliver-skillshub-adonisjs && rm -rf "$T"
manifest:
skills/TerminalSkills/skills/adonisjs/SKILL.mdsource content
AdonisJS
Overview
AdonisJS is a full-stack Node.js framework — the "Laravel of Node.js." Unlike Express/Fastify where you assemble everything from packages, AdonisJS ships with an ORM (Lucid), authentication, validation (VineJS), mailer, queues, and testing out of the box. Opinionated, TypeScript-first, and production-ready.
When to Use
- Building a complete web application (not just an API) with Node.js
- Want an opinionated framework with conventions (like Rails/Laravel)
- Need built-in auth (sessions, API tokens, social OAuth)
- Database-driven applications with migrations and models
- Teams that prefer convention over configuration
Instructions
Setup
npm init adonisjs@latest my-app -- --kit=web cd my-app node ace serve --watch
Routes and Controllers
// start/routes.ts — Route definitions import router from "@adonisjs/core/services/router"; const UsersController = () => import("#controllers/users_controller"); router.group(() => { router.get("/users", [UsersController, "index"]); router.get("/users/:id", [UsersController, "show"]); router.post("/users", [UsersController, "store"]); router.put("/users/:id", [UsersController, "update"]); router.delete("/users/:id", [UsersController, "destroy"]); }).prefix("/api");
// app/controllers/users_controller.ts — Controller with validation import type { HttpContext } from "@adonisjs/core/http"; import User from "#models/user"; import { createUserValidator, updateUserValidator } from "#validators/user"; export default class UsersController { async index({ request }: HttpContext) { const page = request.input("page", 1); const limit = request.input("limit", 20); return User.query().paginate(page, limit); } async show({ params }: HttpContext) { return User.findOrFail(params.id); } async store({ request, response }: HttpContext) { const data = await request.validateUsing(createUserValidator); const user = await User.create(data); return response.created(user); } async update({ params, request }: HttpContext) { const user = await User.findOrFail(params.id); const data = await request.validateUsing(updateUserValidator); user.merge(data); await user.save(); return user; } async destroy({ params, response }: HttpContext) { const user = await User.findOrFail(params.id); await user.delete(); return response.noContent(); } }
Lucid ORM (Models and Migrations)
// app/models/user.ts — Lucid model with relationships import { DateTime } from "luxon"; import { BaseModel, column, hasMany } from "@adonisjs/lucid/orm"; import type { HasMany } from "@adonisjs/lucid/types/relations"; import Post from "#models/post"; export default class User extends BaseModel { @column({ isPrimary: true }) declare id: number; @column() declare email: string; @column() declare name: string; @column({ serializeAs: null }) // Never include in JSON declare password: string; @hasMany(() => Post) declare posts: HasMany<typeof Post>; @column.dateTime({ autoCreate: true }) declare createdAt: DateTime; @column.dateTime({ autoCreate: true, autoUpdate: true }) declare updatedAt: DateTime; }
# Generate migration node ace make:migration create_users_table # Run migrations node ace migration:run # Rollback node ace migration:rollback
VineJS Validation
// app/validators/user.ts — Request validation with VineJS import vine from "@vinejs/vine"; export const createUserValidator = vine.compile( vine.object({ email: vine.string().email().unique(async (db, value) => { const user = await db.from("users").where("email", value).first(); return !user; // Must not exist }), name: vine.string().minLength(2).maxLength(100), password: vine.string().minLength(8).confirmed(), // password + password_confirmation }) ); export const updateUserValidator = vine.compile( vine.object({ name: vine.string().minLength(2).maxLength(100).optional(), email: vine.string().email().optional(), }) );
Authentication
// config/auth.ts — Auth config (sessions + API tokens) import { defineConfig } from "@adonisjs/auth"; import { sessionGuard, sessionUserProvider } from "@adonisjs/auth/session"; import { tokensGuard, tokensUserProvider } from "@adonisjs/auth/access_tokens"; export default defineConfig({ default: "web", guards: { web: sessionGuard({ useRememberMeTokens: true, provider: sessionUserProvider({ model: () => import("#models/user") }) }), api: tokensGuard({ provider: tokensUserProvider({ model: () => import("#models/user"), tokens: "accessTokens" }) }), }, });
// app/controllers/auth_controller.ts export default class AuthController { async login({ request, auth, response }: HttpContext) { const { email, password } = request.only(["email", "password"]); const user = await User.verifyCredentials(email, password); await auth.use("web").login(user); return response.redirect("/dashboard"); } async apiToken({ request, auth }: HttpContext) { const { email, password } = request.only(["email", "password"]); const user = await User.verifyCredentials(email, password); const token = await User.accessTokens.create(user); return { token: token.toJSON() }; } }
Examples
Example 1: Full CRUD web application
User prompt: "Build a blog with AdonisJS — posts CRUD, user auth, and server-rendered pages."
The agent will scaffold an AdonisJS web app with Lucid models (User, Post), authentication middleware, VineJS validation, and Edge templates for the views.
Example 2: REST API with token auth
User prompt: "Create a REST API with AdonisJS, API token authentication, and pagination."
The agent will set up API routes, access token guard, JSON responses with pagination, and Japa tests.
Guidelines
- Use
for everything — generators, migrations, REPL, deploymentnode ace - Controllers stay thin — move business logic to services
- VineJS for all input — never trust
directlyrequest.body()
— exclude sensitive fields (password) from JSON serializationserializeAs: null- Lucid relations are lazy — use
or.preload()
to eager load.query().preload() - Migrations are sequential — don't modify old migrations; create new ones
- Auth middleware —
router.get('/dashboard').use(middleware.auth()) - Testing with Japa — built-in test runner, no Jest needed
- Deploy:
→ runnode ace build
in productionbuild/server.js