Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

A JSON API and access tokens

A mobile app or a React frontend needs data in JSON, not HTML pages. An API is a set of addresses that accept and return JSON. Sessions and cookies are awkward for such clients, so the user gets an access token — a long random string the client sends in a header with every request.

API routes

A controller that returns an object or a model already responds with JSON:

// app/controllers/api/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import Post from '#models/post'
 
export default class PostsController {
  async index({}: HttpContext) {
    return Post.query().where('is_published', true).orderBy('created_at', 'desc')
  }
 
  async show({ params }: HttpContext) {
    return Post.findOrFail(params.id)
  }
}
// start/routes.ts
const ApiPostsController = () => import('#controllers/api/posts_controller')
 
router
  .group(() => {
    router.get('/posts', [ApiPostsController, 'index'])
    router.get('/posts/:id', [ApiPostsController, 'show'])
  })
  .prefix('/api')

Open /api/posts — a JSON array of posts appears. Model properties arrive in camelCase: isPublished, createdAt.

The access_tokens guard

Tokens are stored in the auth_access_tokens table. If the project was created with the API kit, everything is set up. In the Web kit add a guard to config/auth.ts next to web:

// config/auth.ts
import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens'
 
// inside guards:
api: tokensGuard({
  provider: tokensUserProvider({
    tokens: 'accessTokens',
    model: () => import('#models/user'),
  }),
}),

Declare the token store in the user model:

// app/models/user.ts
import { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
 
export default class User extends compose(BaseModel, AuthFinder) {
  // ...columns
 
  static accessTokens = DbAccessTokensProvider.forModel(User)
}

Create the migration for the tokens table with node ace make:migration auth_access_tokens, following the sample from the auth package docs.

Issuing a token

// app/controllers/api/tokens_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import User from '#models/user'
 
export default class TokensController {
  async store({ request }: HttpContext) {
    const { email, password } = request.only(['email', 'password'])
    const user = await User.verifyCredentials(email, password)
    const token = await User.accessTokens.create(user)
 
    return { type: 'bearer', value: token.value!.release() }
  }
}

release() returns the token itself as a string like oat_.... It is shown only once — the database keeps only its hash.

Protected routes

// start/routes.ts
import { middleware } from '#start/kernel'
 
router
  .post('/api/posts', [ApiPostsController, 'store'])
  .use(middleware.auth({ guards: ['api'] }))

The client passes the token in the Authorization header:

curl -X POST http://localhost:3333/api/posts \
  -H "Authorization: Bearer oat_MQ.abc123" \
  -H "Accept: application/json" \
  -d "title=From the API&body=A post created through the API"

Without a token the response is 401; with a valid token auth.user is available inside the controller.

In the Web kit CSRF protection rejects such a POST, because an API client has no CSRF token. Exclude the API addresses in config/shield.ts:

// config/shield.ts
csrf: {
  enabled: true,
  exceptRoutes: (ctx) => ctx.request.url().startsWith('/api/'),
  // ...other settings
},

Common mistake

The API client does not send the Accept: application/json header. Then on a validation error AdonisJS treats the request as a regular form and responds with a redirect instead of JSON with status 422. Make the client always send Accept: application/json — then errors come back as JSON too.

Thanks for reading!

Donate to the site — we work hard for you.