
Your Nuxt app needs to use the Repository Pattern (or at least something like it). If you don't, and build your app the naive way, you'll be coupled to your database. And that will cause you *so* much pain down the road.

Get notified when we release new tutorials, lessons, and other expert Nuxt content.
With the Repository Pattern, your database and server routes will be so neatly decoupled and separated that it will make it a breeze to:
// Before: Mixed concerns in route
export default defineEventHandler(async (event) => {
const chats = await database.query('SELECT * FROM chats')
// Route knows about database details!
})
// After: Clean separation
export default defineEventHandler(async (event) => {
const chats = await getAllChats()
// Route just calls repository method
})
Before, your server routes contain all sorts of database-specific logic.
After, there's a clean separation of concerns which makes maintaining your app so much better.
Now that I've convinced you, let's dive into how we actually implement this in a Nuxt app.
First, you'll need to create a new folder inside your server/ directory: server/repository/.
Inside, you'll start with one file per domain (eg. chatRepository.ts, projectRepository.ts). You'll split these files up into more focused files as you need to.
Your file structure will look something like this:
server/
├── api/
│ ├── chats/
│ │ └── index.get.ts
│ └── projects/
│ └── index.get.ts
└── repository/
├── chatRepository.ts
└── projectRepository.ts
Taking it a step further, we can organize these into separate layers to make this even more modular:
layers/
├── chats/
│ └── server/
│ ├── api/
│ │ └── chats/
│ │ └── index.get.ts
│ └── repository/
│ └── chatRepository.ts
├── projects/
│ └── server/
│ ├── api/
│ │ └── projects/
│ │ └── index.get.ts
│ └── repository/
│ └── projectRepository.ts
Here, we're creating layers based on domain, following a domain-driven-design approach.
Once you have the main structure set up, you'll implement each of the files.
Here, you really want to focus on making sure your API is consistent and well thought out. This is the part that you won't be able to change easily, and getting this part right will make swapping out implementations a breeze in the future.
I like to follow these rules:
async functions for all CRUD operations (create, read, update, delete)Your interface will look something like this:
// chatRepository.ts
export async function getAllChats(): Promise<Chat[]> { }
export async function getChatById(id: string): Promise<Chat | null> { }
export async function createChat(data: CreateChatData): Promise<Chat> { }
export async function updateChat(id: string, data: UpdateChatData): Promise<Chat | null> { }
export async function deleteChat(id: string): Promise<boolean> { }
In the chatRepository.ts file we have all the main CRUD operations for the chat resource:
idBefore we get to actually implementing these server routes, we'll first see how we'll use these repository methods in our server routes so we get the full end-to-end picture.
In our server routes, all we need to do is import and call these functions:
// server/api/chats/index.get.ts
import { getAllChats } from '~/server/repository/chatRepository'
export default defineEventHandler(async (event) => {
return await getAllChats()
})
Our server routes are no longer concerned with the details of how the chat data is stored, how to fetch it, or what kind of transformation might be needed to send our response back.
The only things our server routes need to deal with now are all of the HTTP method stuff. Parsing the body or parameters from the query, checking or setting cookies, catching and handling errors with the right status codes, and so on.
This makes our server routes just a thin layer that allows our actual business logic (in the repository files) be accessed through a web server. This also means we could easily build any kind of interface we want for this repository layer, like a CLI tool or Nitro task.
Let's say that our current implementation of getAllChats is just in-memory, because we wanted to keep things really simple:
const chats: Chat[] = []
export async function getAllChats(): Promise<Chat[]> {
return chats
}
Obviously, this doesn't make for a great app, since every server restart completely resets everything!
So, we refactor to use unstorage which is built in to Nuxt through the useStorage composable:
export async function getAllChats(): Promise<Chat[]> {
const storage = useStorage('chats')
const keys = await storage.getKeys()
const chats = await Promise.all(
keys.map(key => storage.getItem(key))
)
return chats
}
Aside from some configuration to set up unstorage, this is all that we have to do, because we've nicely abstracted this away.
Our server route for listing all chats stays exactly the same, because we didn't change the interface of this method, only the internal implementation details. And, if we're using this method in other server routes (which we probably are), we've saved ourselves a ton of headache already.
We can even completely change how we store our chats, as long as we don't change the method interface:
// Store all chats in one object: { [id: string]: Chat }
export async function getAllChats(): Promise<Chat[]> {
const storage = useStorage('data')
const chatsObject = await storage.getItem('chats') || {}
// Convert object to array
return Object.values(chatsObject)
}
Through the Repository Pattern here, we've neatly encapsulated all of our business logic and data storage into it's own layer, making maintenance and adding new features so much easier.
Because all of the data access is in a few files, we have very little to update if we want to completely swap out our database or change our database schema.
And writing new server routes is now only about stringing together repository methods, once you've parsed your inputs and handled all the errors.
