Skip to main content
Engineering8 min readJanuary 15, 2026

Building a SaaS Application with Nuxt and TypeScript

Nuxt gives you server-side rendering, file-based routing, and full-stack TypeScript in one framework. Here's how to structure a real SaaS application with it.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Why Nuxt for SaaS

Most SaaS applications are B2B dashboards — authenticated experiences where users manage data, configure settings, and view reports. These applications don't need the aggressive SEO optimization that drives server-side rendering adoption, which is why many teams default to client-side-only frameworks.

But Nuxt offers more than SSR. Its architecture — file-based routing, auto-imported composables, server routes with Nitro, and first-class TypeScript support — provides a structure that scales well with application complexity. As a SaaS product grows from a handful of pages to dozens of feature areas, Nuxt's conventions prevent the structural entropy that plagues large single-page applications.

I've built several SaaS applications with Nuxt, including a multi-tenant ERP platform, and the framework's opinions about project structure have consistently saved time over the lifetime of each project. The initial learning curve pays for itself by the time you have 30 pages and 50 components.


Project Structure for SaaS

Nuxt's default directory structure works for marketing sites, but a SaaS application needs some adjustments to accommodate authentication, multi-tenancy, and feature organization.

Route organization should mirror your product's information architecture. For a SaaS application, this typically means a public area (marketing pages, login, signup), an authenticated application area, and an admin area. Nuxt's layout system handles this cleanly — define a default layout for the marketing site, an app layout with sidebar navigation for the authenticated experience, and an admin layout for administrative functions.

Feature-based component organization prevents the components directory from becoming a flat list of hundreds of files. Group components by feature area: components/billing/, components/settings/, components/projects/. Nuxt auto-imports them with a prefix based on the directory name, so components/billing/PlanSelector.vue is available as <BillingPlanSelector /> without manual imports.

Composables for shared logic replace the utility function files that accumulate in other frameworks. composables/useAuth.ts for authentication state and methods, composables/useTenant.ts for current tenant context, composables/usePermissions.ts for role-based access checks. Nuxt auto-imports these, so any component can use useAuth() without an import statement.

Server routes for API endpoints live in server/api/ and run on the Nitro server. For a full-stack Nuxt SaaS application, this is where your business logic lives. Each server route is a TypeScript function that receives a request and returns a response. You get type safety from the route handler to the component that consumes the data, with no API client generation required.


Authentication and Middleware

Authentication is the first thing a SaaS application needs, and Nuxt's middleware system provides a clean way to enforce it.

Route middleware runs before navigation and can redirect unauthenticated users. Define a global auth middleware that checks for a valid session and redirects to /login if none exists. Apply it selectively — marketing pages and the login page are public, everything under /app/ requires authentication.

Server middleware protects API routes. Every route in server/api/ that handles tenant-specific data needs to verify the session token, extract the user and tenant context, and make it available to the route handler. A shared utility function that extracts and validates the session keeps this logic DRY.

Session management with Nuxt can use HTTP-only cookies for session tokens, which the server middleware validates on each request. The useAuth() composable on the client side tracks the current user state and provides login, logout, and session refresh methods. The composable calls server routes for authentication operations, keeping sensitive logic (token validation, password hashing) on the server.

For role-based access control, a usePermissions() composable checks the current user's role against required permissions before rendering sensitive UI elements. The server routes independently verify permissions — client-side permission checks are a UX convenience, not a security mechanism. I've written extensively about designing RBAC systems that enforce permissions at both layers.


Data Fetching Patterns

Nuxt's useFetch and useAsyncData composables handle data fetching with built-in loading states, error handling, and caching. For a SaaS application, a few patterns make these more effective.

Typed API responses ensure that the data returned by useFetch is correctly typed. Define TypeScript interfaces for your API responses and use them with useFetch<ResponseType>(). This gives you type safety from the server route through the component template.

Optimistic updates improve perceived performance for mutations. When a user updates a setting, immediately reflect the change in the UI and send the API request in the background. If the request fails, revert the change and show an error. Nuxt's useAsyncData with manual refresh makes this pattern straightforward.

Pagination and infinite scroll are necessary for list views that display tenant data. Nuxt's useFetch can be combined with reactive query parameters to implement cursor-based pagination. As the user scrolls or clicks "next page," the query parameters update and useFetch automatically re-fetches with the new cursor.

Error handling should use Nuxt's NuxtErrorBoundary component to catch rendering errors and display graceful fallbacks. API errors from useFetch are available via the error ref and should be displayed inline rather than swallowed. For a SaaS product, a user who encounters a silent error is a user who loses trust.


State Management with Pinia

Pinia is the official state management library for Nuxt, and for SaaS applications it handles the global state that doesn't belong to any single component.

Tenant store holds the current tenant's configuration — their plan, their feature flags, their branding settings (if you're building a white-label product). This store is populated on initial page load and consulted throughout the application.

User store holds the current user's profile, preferences, and permissions. It's populated after authentication and cleared on logout.

Feature stores manage domain-specific state. A project management SaaS might have a useProjectStore that holds the current project and its associated data. The key principle is that stores hold shared state — state that multiple components need to access or modify. Component-local state stays in the component.

Persistence for stores that need to survive page refreshes uses the pinia-plugin-persistedstate package, which serializes store state to localStorage or sessionStorage. Use this sparingly — persisting too much state leads to stale data bugs. Authentication state and user preferences are good candidates. Business data should be re-fetched from the server.

Nuxt with TypeScript, Pinia, and Nitro server routes gives you a full-stack, type-safe development experience in a single project. The framework's opinions about structure prevent the architectural drift that makes large applications hard to maintain, and the auto-import system reduces boilerplate without sacrificing discoverability.


Keep Reading