This repository contains a Next.js application integrated with AWS Amplify, providing a robust backend with server-side rendering (SSR). It is designed as a sandbox environment for rapid prototyping and testing. The project follows the guidelines provided in the Amplify Next.js App Router with Server Components Guide and implements the latest Next.js v15 (React 19) features.
- Features
- Prerequisites
- Getting Started
- AWS Amplify Setup
- Local Development
- Amplify Authenticator Component
- Clean Architecture
- Deployment
- Troubleshooting
- Server-Side Rendering (SSR): Utilizes Next.js for fast, SEO-friendly pages.
- AWS Amplify Integration: Simplifies backend management for authentication, APIs, and storage.
- Sandbox Environment: Ideal for testing and development, with the ability to switch between multiple environments.
- Extensible Architecture: Easily extend the app with additional backend services or frontend components.
Before setting up the project, ensure you have the following installed:
- Node.js: Download (runtime for JavaScript).
- npm: Download (comes with Node.js).
- Git: Download (to clone the repository).
- AWS Account: Sign Up (for Amplify services).
-
Clone the Repository
git clone https://github.com/matt-wigg/aws-amplify-next-js-clean-architecture.git cd aws-amplify-next-js-clean-architecture
-
Install Dependencies
npm install
Important
Before running the application, you need to set up AWS Amplify for local development. Follow the official AWS documentation for configuring your AWS account and setting up IAM Identity Center - Configure AWS for Local Development.
You can manage Amplify sandboxes in two ways:
Located in infrastructure/amplify/scripts/
, these scripts simplify common sandbox operations with interactive prompts.
cd infrastructure
npm run amplify
This will:
- Prompt for your AWS profile (e.g. aws-dev-environment)
- Prompt for a sandbox identifier (optional)
cd infrastructure
npm run amplify:delete
This will:
- Prompt for the same profile and optional identifier
- Ask for confirmation before deleting resources
Use these when you need full control or automation in CI/CD pipelines.
cd infrastructure
npx ampx sandbox --profile <your-profile-name> --identifier <optional-custom-id>
cd infrastructure
npx ampx sandbox delete --profile <your-profile-name> --identifier <optional-custom-id>
By default, the sandbox --identifier
is set to your system username. If you start a sandbox with a custom identifier, you must also delete it using the same identifier.
Note
Please see the official Amplify documentation for more information on how to use cloud sandboxes in dev environments.
-
Start the Next.js Development Server
npm run dev
or start with turbopack - a faster development server:
npm run turbo-dev
-
Start the Amplify Sandbox
cd infrastructure npm run amplify
-
Access the Application
Your application will be available at http://localhost:3000.
This project utilizes the AWS Amplify Authenticator UI component to manage user authentication seamlessly. The Authenticator provides pre-built, customizable authentication flows, including sign-up, sign-in, and multi-factor authentication, reducing the need for extensive boilerplate code.
You can customize the Authenticator component by providing custom components for the header, footer, and other UI elements.
// @nextjs/components/auth/cognito-authenticator
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { Authenticator, useAuthenticator } from "@aws-amplify/ui-react";
import Image from "next/image";
import Logo from "@public/logo.webp";
import { ROUTES } from "@nextjs/constants/routes.constants";
/**
* Custom UI components for the Amplify Authenticator.
* Includes Matt Wigg-branded header and a global footer.
*/
const customComponents = {
Header() {
return (
<figure className="w-30 h-30 mx-auto border border-border relative overflow-hidden rounded-full mb-8">
<Image
src={Logo}
alt="Matt Wigg Logo"
fill
sizes="96px"
style={{ objectFit: "cover" }}
priority
quality={85}
/>
</figure>
);
},
Footer() {
return (
<p className="text-sm text-center text-muted-foreground mt-4">
© {new Date().getFullYear()} Matt Wigg. All Rights Reserved.
</p>
);
},
};
/**
* CognitoAuthenticator component.
* Renders the Amplify Authenticator with Matt Wigg branding and handles routing after sign in.
*/
export function CognitoAuthenticator() {
const { authStatus } = useAuthenticator((context) => [context.authStatus]);
const router = useRouter();
useEffect(() => {
if (authStatus === "authenticated") {
router.replace(ROUTES.INTERNAL.HOME);
}
}, [authStatus]);
return <Authenticator components={customComponents} initialState="signIn" />;
}
This project follows Uncle Bob’s Clean Architecture principles to enforce a clear separation of concerns and maintain high scalability and testability.
Clean Architecture promotes the following layered structure:
-
Domain Layer (Entities & Interfaces):
- Contains pure business logic and core models (e.g.
Todo
,User
, etc.). - Defines interfaces for repositories, not implementations.
- Independent of frameworks, databases, or UI.
- Contains pure business logic and core models (e.g.
-
Infrastructure Layer (Frameworks, Drivers):
- Implementation details (e.g. AWS Amplify, cookies, API clients).
- Injected into the system via interfaces, never accessed directly by use cases.
-
Application Layer (Use Cases):
- Contains business use cases that orchestrate operations (e.g.
createTodo
,getCurrentUser
). - Coordinates domain entities and repository interfaces.
- No knowledge of external dependencies.
- Contains business use cases that orchestrate operations (e.g.
-
Interface Adapters (Controllers, Presenters):
- Adapts data between the application layer and the outside world.
- Includes controllers (input), presenters (output), and formatters.
- Converts raw results into view-ready responses.
The following example demonstrates how the getTodos
functionality flows through all architectural layers:
// @domain/models/Todo.ts
type Todo = {
id: string;
content: string;
order?: number;
// ...other properties
}
// @domain/interfaces/todo.repository.interface.ts
interface ITodoRepository {
list(): Promise<Todo[]>;
// ...other methods
}
// @infrastructure/repositories/todo.repository.ts
import { cookiesClient } from "@infrastructure/utils/amplify.utils";
import type { ITodoRepository } from "@domain/repositories/todo.interface";
export const todoRepository: ITodoRepository = {
async list(): Promise<Todo[]> {
try {
const { data } = await cookiesClient.models.Todo.list({});
return data ?? [];
} catch (err) {
console.error("TodoRepository.list error", err);
return [];
}
},
// ...other methods
};
// @application/use-cases/todo/get-todos.ts
import type { ITodoRepository } from "@domain/repositories/todo.interface";
import type { Todo } from "@domain/models/Todo";
export async function getTodos(repo: ITodoRepository): Promise<Todo[]> {
return repo.list();
}
// @interfaceadapters/controllers/todo/todo.controller.ts
import { getTodos } from "@application/use-cases/todo/get-todos";
import { todoRepository } from "@infrastructure/repositories/todo.repository";
export const TodoController = {
async getTodos(): Promise<Todo[]> {
return getTodos(todoRepository);
},
// ...other methods
};
// @interfaceadapters/presenters/todo/todo.presenter.ts
export const TodoPresenter = {
presentSortedTodos(todos: Todo[]): Todo[] {
if (todos.length === 0) {
return [];
}
return [...todos].sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
},
};
// @nextjs/app/(authenticated)/draggable/page.tsx
"use server";
import { TodoController } from "@interface-adapters/controllers/todo/todo.controller";
import { TodoPresenter } from "@interface-adapters/presenters/todo/todo.presenter";
import { DraggableTodoList } from "@nextjs/components/draggable/draggable-todo-list";
export default async function DraggablePage() {
// Controller retrieves data from use case
const todos = await TodoController.getTodos();
// Presenter formats data for UI consumption
const sortedTodos = TodoPresenter.presentSortedTodos(todos);
return (
<main>
{/* Components receive and display the prepared data */}
<DraggableTodoList initialTodos={sortedTodos} />
</main>
);
}
- Testable: Core business logic is isolated and can be unit tested independently of infrastructure or UI.
- Framework-Agnostic: You can replace AWS Amplify, Next.js, or UI libraries with minimal refactoring, thanks to clear abstractions.
- Maintainable: Well-defined boundaries ensure that changes in one layer do not create unintended side effects in others.
- Extensible: The modular structure makes it straightforward to add new features, integrations, or swap implementations as requirements evolve.
- 🧠 The Clean Architecture - Uncle Bob (Blog)
- 📘 Clean Architecture Book - Robert Martin (Uncle Bob)
- 🎥 Clean Architecture Explained - Jason Taylor
Amplify supports deployment and hosting for server-side rendered (SSR) web apps created using Next.js. Please see the Amplify documentation: Amplify support for Next.js.
Amplify code-first DX (Gen 2) offers fullstack branch deployments that allow you to automatically deploy infrastructure and application code changes from feature branches. Fullstack Branch Deployments.
Issue | Solution |
---|---|
Amplify CLI Issues |
Update with: npm install -g @aws-amplify/cli |
Build Failures |
Ensure Node.js and npm versions match the project requirements. |
Amplify Sandbox Issues |
Check Amplify Sandbox setup and AWS credentials. |