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.