js

How to Build Secure, Scalable APIs with AdonisJS and Node.js

Learn how to create fast, secure, and production-ready APIs using AdonisJS with built-in authentication, validation, and database tools.

How to Build Secure, Scalable APIs with AdonisJS and Node.js

I’ve been thinking about building APIs lately. Not just any APIs, but ones that feel solid, reliable, and ready for real people to use. You know the feeling when you’re using an app and everything just works? The buttons do what you expect, your data is safe, and things load quickly. That’s the experience I want to create for other developers. So, I started looking for a tool that could help me build that experience from the ground up, without getting lost in endless configuration. That’s how I found AdonisJS.

It’s a framework for Node.js that comes with a clear plan. Instead of spending days deciding how to structure your project or which library to use for authentication, AdonisJS gives you sensible defaults. It’s like having a seasoned architect help you lay the foundation. You can still customize everything, but you start from a position of strength. The built-in tools, especially for talking to databases and checking user input, mean you can focus on what makes your application unique.

Why does this matter now? Because we’re building more complex applications than ever. Users expect speed, security, and reliability. A shaky API can break an entire product. I wanted to find a way to build APIs that are strong from day one, and that’s the journey I want to share with you.

Let’s start by setting up a new project. Open your terminal and run a simple command. This will create a new API-focused project with authentication already set up.

npm init adonisjs@latest my-solid-api

When prompted, choose the ‘api’ project structure and say yes to setting up authentication. For the database, select PostgreSQL. This gives us a great starting point. Once it’s done, move into your new project folder.

The project has a clear structure right away. You’ll see folders for controllers, models, and database migrations. This organization is helpful. It means you always know where to find your code. The config folder holds settings for your database, authentication, and other services. Everything has its place.

Now, let’s connect to a database. We need to tell AdonisJS where our PostgreSQL database lives. Open the .env file in the root of your project. You’ll add your database credentials here.

DB_HOST=localhost
DB_PORT=5432
DB_USER=postgres_user
DB_PASSWORD=your_secure_password
DB_DATABASE=my_solid_api

Next, we need to create a model. A model is a blueprint for the data in your application. Let’s say we’re building a task manager. We’ll need a Task model. AdonisJS makes this easy with a command-line tool.

node ace make:model Task -m

This command does two things. It creates a model file in app/Models/Task.ts and a migration file in database/migrations/. The migration is a script that builds the table in your database. Open the migration file. You’ll see a schema where you can define your table’s columns.

// database/migrations/1730..._create_tasks_table.ts
import { BaseSchema } from '@adonisjs/lucid/schema'

export default class extends BaseSchema {
  protected tableName = 'tasks'

  async up() {
    this.schema.createTable(this.tableName, (table) => {
      table.increments('id').primary()
      table.string('title').notNullable()
      table.text('description').nullable()
      table.boolean('is_completed').defaultTo(false)
      table.integer('user_id').unsigned().references('id').inTable('users')
      table.timestamp('created_at', { useTz: true })
      table.timestamp('updated_at', { useTz: true })
    })
  }

  async down() {
    this.schema.dropTable(this.tableName)
  }
}

See how we define a user_id that links to a users table? This sets up a relationship. A task belongs to a user. Now, run the migration to create the table in your database.

node ace migration:run

With the table ready, let’s look at the model file. This is where the real power starts. The model allows us to interact with our data in a clean, object-oriented way.

// app/Models/Task.ts
import { DateTime } from 'luxon'
import { BaseModel, belongsTo, column } from '@adonisjs/lucid/orm'
import type { BelongsTo } from '@adonisjs/lucid/types/relations'
import User from './user.js'

export default class Task extends BaseModel {
  @column({ isPrimary: true })
  declare id: number

  @column()
  declare title: string

  @column()
  declare description: string | null

  @column()
  declare isCompleted: boolean

  @column()
  declare userId: number

  @column.dateTime({ autoCreate: true })
  declare createdAt: DateTime

  @column.dateTime({ autoCreate: true, autoUpdate: true })
  declare updatedAt: DateTime

  @belongsTo(() => User)
  declare user: BelongsTo<typeof User>
}

The @belongsTo decorator defines that relationship we set up in the database. This is part of Lucid ORM, AdonisJS’s built-in tool for databases. It lets you write queries that feel natural.

How do you actually use this model? Let’s say we’re in a controller handling an API request. We can fetch all tasks for the logged-in user with a simple query.

// app/Controllers/Http/TasksController.ts
import Task from '#models/task'

export default class TasksController {
  async index({ auth }) {
    const user = auth.user!
    const tasks = await Task.query().where('user_id', user.id)
    return tasks
  }
}

But what about creating a new task? We can’t just take any data from the request and save it. We need to check it first. This is where validation comes in. Bad data can break your application. AdonisJS includes a powerful validation library called VineJS.

We create a validator to define the rules for a new task. This keeps our controller clean and our rules reusable.

// app/Validators/TaskValidator.ts
import vine from '@vinejs/vine'

export const createTaskValidator = vine.compile(
  vine.object({
    title: vine.string().minLength(3).maxLength(255),
    description: vine.string().maxLength(1000).optional(),
    isCompleted: vine.boolean().optional(),
  })
)

Now, in our controller, we can validate the request before doing anything else.

// In TasksController.ts
async store({ request, response, auth }) {
  const user = auth.user!
  const data = await request.validateUsing(createTaskValidator)

  const task = await Task.create({
    ...data,
    userId: user.id,
  })

  return response.created(task)
}

If the validation fails, AdonisJS automatically sends a clear error response back to the client. You don’t have to write that logic yourself. This is what I mean by building from a position of strength. The framework handles the common, tedious problems for you.

Security is non-negotiable. AdonisJS has a built-in authentication system. When you created the project, it set up everything you need for users to log in and register. Protecting a route is straightforward. You just add a middleware.

// start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/tasks', '#controllers/tasks_controller.index').middleware('auth')

The auth middleware ensures only logged-in users can access that endpoint. It checks for a valid API token or session. This system supports multiple methods, like tokens and sessions, so you can choose what fits your app.

Have you ever wondered how to handle complex operations safely, like transferring points between users? You need to ensure both the deduction and the addition happen, or neither does. This is where database transactions are essential. Lucid ORM makes using transactions simple.

async transferPoints({ fromUser, toUser, points }) {
  const trx = await Database.transaction()

  try {
    await fromUser.related('account').query().useTransaction(trx).decrement('balance', points)
    await toUser.related('account').query().useTransaction(trx).increment('balance', points)

    await trx.commit()
    return { success: true }
  } catch (error) {
    await trx.rollback()
    return { success: false, error }
  }
}

If anything goes wrong inside the try block, the rollback method undoes all the changes. This keeps your data consistent.

As your API grows, you’ll need to manage how it changes over time. API versioning is a common strategy. You can keep supporting old clients while adding new features. In AdonisJS, you can organize this by prefixing your routes.

// start/routes.ts
router.group(() => {
  router.get('/tasks', '#controllers/tasks_controller.index')
  router.post('/tasks', '#controllers/tasks_controller.store')
}).prefix('/api/v1')

Later, you can add a new set of routes under /api/v2. Your old clients keep using v1, and new ones can use v2. It’s a clean way to evolve.

Finally, you need to get your API out into the world. Deployment should be simple. AdonisJS works well with process managers like PM2, which keep your application running and restart it if it crashes.

npm install -g pm2
pm2 start build/server.js --name my-solid-api

You can also use Docker to create a consistent environment from your laptop to the server. A basic Dockerfile gets you started.

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3333
CMD ["node", "build/server.js"]

Building an API is more than just making endpoints. It’s about creating a reliable service that other developers can trust. AdonisJS gives you the tools to build that trust from the start. You spend less time on setup and more time on what makes your application special.

I hope this guide helps you build something great. What part of API building are you most curious about? If you found this useful, please share it with someone else who might be on a similar journey. I’d love to hear about what you’re building in the comments below. Let’s keep the conversation going.


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: adonisjs,api development,nodejs,authentication,rest api



Similar Posts
Blog Image
How to Use Joi with Fastify for Bulletproof API Request Validation

Learn how to integrate Joi with Fastify to validate API requests, prevent bugs, and secure your backend with clean, reliable code.

Blog Image
How to Build a Scalable Video Conferencing App with WebRTC and Node.js

Learn how to go from a simple peer-to-peer video call to a full-featured, scalable conferencing system using WebRTC and Mediasoup.

Blog Image
Build Fast, Type-Safe APIs with Bun, Elysia.js, and Drizzle ORM

Learn how to create high-performance, type-safe APIs using Bun, Elysia.js, and Drizzle ORM with clean architecture and instant feedback.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Database Operations

Learn how to integrate Next.js with Prisma ORM for type-safe, scalable web apps. Build full-stack applications with seamless database interactions and TypeScript support.

Blog Image
Build Production-Ready GraphQL API with NestJS, Prisma and Redis Caching - Complete Tutorial

Learn to build scalable GraphQL APIs with NestJS, Prisma, and Redis caching. Master authentication, real-time subscriptions, and production deployment.

Blog Image
Build Production-Ready Type-Safe Microservices: Complete tRPC, Prisma, and Docker Tutorial

Learn to build type-safe microservices with tRPC, Prisma & Docker. Complete production guide with authentication, testing & deployment strategies.