How to Build a Full-Stack Application with Next.js and AdonisJS (Full TypeScript Setup)

Landry Bella
Next.jsTypeScriptauthenticationadonisjs
How to Build a Full-Stack Application with Next.js and AdonisJS (Full TypeScript Setup)

Building a full-stack application with Next.js and AdonisJS using TypeScript ensures type safety across your project, making your code more maintainable and reducing potential errors. This guide will show you how to fully leverage TypeScript in both the front-end and back-end and provide detailed steps to set up authentication for secure, real-world applications.

Why Choose Next.js and AdonisJS with TypeScript?

  • Next.js: A React-based framework offering server-side rendering, static site generation, and built-in TypeScript support.
  • AdonisJS: A TypeScript-first back-end framework inspired by Laravel, providing routing, ORM, and authentication out of the box.
  • TypeScript: Provides strong typing and tooling for a seamless developer experience.

Set Up the AdonisJS Back-End with TypeScript

AdonisJS natively supports TypeScript, making it a great choice for modern back-end development.

  • Step 1: Install AdonisJS CLI and Create a Project

Install the CLI globally:

1 npm install -g @adonisjs/cli

Create a new project using the API boilerplate:

1 2 adonis new backend-app --typescript cd backend-app
  • Step 2: Configure the Database

Update the .env file with your database credentials:

1 2 3 4 5 6 DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_USER=root DB_PASSWORD=yourpassword DB_DATABASE=adonis

Run the migrations to set up the database schema:

1 node ace migration:run
  • Step 3: Test the Server

Start the development server:

1 npm run dev

The server will run at http://localhost:3333.

Set Up Authentication in AdonisJS

Authentication is a key part of any application. AdonisJS provides a flexible authentication system that supports JWT, session-based authentication, and API tokens.

  • Step 1: Install and Configure Authentication

Install the authentication package:

1 2 npm install @adonisjs/auth node ace configure @adonisjs/auth

Choose "API Tokens" for a typical full-stack app. This will update the project with:

User model and migration.

1 Auth config file (config/auth.ts).

Run the migration to create the users table:

1 node ace migration:run
  • Step 2: Create Auth Routes

Add routes for user registration, login, and fetching user details in start/routes.ts:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import Route from '@ioc:Adonis/Core/Route'; import Hash from '@ioc:Adonis/Core/Hash'; import User from 'App/Models/User'; Route.post('/register', async ({ request }) => { const data = request.only(['email', 'password']); const user = await User.create(data); return user; }); Route.post('/login', async ({ auth, request, response }) => { const { email, password } = request.only(['email', 'password']); const user = await User.findByOrFail('email', email); if (!(await Hash.verify(user.password, password))) { return response.unauthorized('Invalid credentials'); } const token = await auth.use('api').generate(user); return { token }; }); Route.get('/me', async ({ auth }) => { return auth.user; }).middleware('auth');
  • Step 3: Test the API

Use a tool like Postman or curl to test the API endpoints:

1 2 3 Register: POST /register with { "email": "user@example.com", "password": "secret" }. Login: POST /login with { "email": "user@example.com", "password": "secret" }. Get User Info: GET /me with the Authorization: Bearer header.

Set Up the Next.js Front-End with TypeScript

Next.js has built-in TypeScript support.

  • Step 1: Create a TypeScript Project
1 2 npx create-next-app frontend-app --typescript cd frontend-app
  • Step 2: Install Axios

Install Axios for API interactions:

1 npm install axios
  • Step 3: Configure API Client

Create a reusable Axios instance with TypeScript support in utils/api.ts:

1 2 3 4 5 6 7 import axios from 'axios'; const api = axios.create({ baseURL: 'http://localhost:3333', // AdonisJS server URL }); export default api;
  • Step 4: Add Type Definitions

Create type definitions for user and authentication responses in types/index.ts:

1 2 3 4 5 6 7 8 9 10 11 12 13 export interface User { id: number; email: string; } export interface LoginResponse { token: string; } export interface RegisterResponse { id: number; email: string; }
  1. Implement Authentication in Next.js
  • Step 1: Create Auth Context

Create an authentication context to manage user state and tokens in contexts/AuthContext.tsx:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 import { createContext, useState, useEffect, ReactNode } from 'react'; import api from '../utils/api'; import { User, LoginResponse } from '../types'; interface AuthContextProps { user: User | null; login: (email: string, password: string) => Promise; logout: () => void; } export const AuthContext = createContext<AuthContextProps | null>(null); export const AuthProvider = ({ children }: { children: ReactNode }) => { const [user, setUser] = useState<User | null>(null); const login = async (email: string, password: string) => { const response = await api.post('/login', { email, password }); localStorage.setItem('token', response.data.token); const userResponse = await api.get('/me', { headers: { Authorization: Bearer ${response.data.token} }, }); setUser(userResponse.data); }; const logout = () => { localStorage.removeItem('token'); setUser(null); }; useEffect(() => { const token = localStorage.getItem('token'); if (token) { api.get('/me', { headers: { Authorization: Bearer ${token} } }) .then((response) => setUser(response.data)) .catch(() => logout()); } }, []); return ( <AuthContext.Provider value={{ user, login, logout }}> {children} </AuthContext.Provider> ); };
  • Step 2: Use Auth Context in Components

Wrap your application with the AuthProvider in pages/_app.tsx:

1 2 3 4 5 6 7 8 9 10 import '../styles/globals.css'; import { AuthProvider } from '../contexts/AuthContext'; function MyApp({ Component, pageProps }: any) { return ( <Component {...pageProps} /> ); } export default MyApp;

Use the context in pages like pages/login.tsx:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { useContext, useState } from 'react'; import { AuthContext } from '../contexts/AuthContext'; export default function LoginPage() { const auth = useContext(AuthContext); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const handleLogin = async () => { try { await auth?.login(email, password); alert('Logged in successfully!'); } catch { alert('Login failed'); } }; return ( <div> <h1>Login</h1> <input type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} /> <input type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} /> <Button onClick={handleLogin}>Login</Button> <div> ); }

Conclusion

By fully utilizing TypeScript, Next.js, and AdonisJS, you’ve built a secure and scalable full-stack application. The strong typing ensures better maintainability and fewer runtime errors, while AdonisJS’s built-in authentication and Next.js’s flexibility make this stack ideal for modern web apps.

If you’d like to expand this project, consider:

  • Adding role-based access control.
  • Using a library like React Query for server state management.
  • Deploying the application to production with services like Vercel and Heroku.

Let me know in the comments if you’d like to explore these advanced topics!