Angular & PocketBase: Full-Stack Synergy for Modern Web Apps
Modern web development often demands a robust frontend paired with an efficient, scalable backend. While traditional full-stack setups can be complex, integrating Angular with PocketBase offers a streamlined, powerful alternative. Angular provides a comprehensive framework for building single-page applications, while PocketBase delivers a full-featured backend (database, authentication, file storage, API) in a single, self-hosted Go executable.
This guide demonstrates how to combine these two technologies to create a dynamic web application, focusing on practical implementation with TypeScript.
1. PocketBase Backend Setup
First, set up your PocketBase instance. Download the appropriate executable from the official website and run it. PocketBase will automatically create a pb_data directory and start a server, typically on http://127.0.0.1:8090.
./pocketbase serve
Access the admin UI at http://127.0.0.1:8090/_/ to create your first collection. For this example, we'll create a "tasks" collection with the following fields:
title(text)isComplete(boolean, defaultfalse)user(relation, touserscollection, single)
Ensure you set appropriate access rules for the tasks collection, allowing authenticated users to create, view, update, and delete their own tasks.
2. Angular Frontend Initialization
Create a new Angular project using the CLI. This sets up the project structure and necessary dependencies.
ng new pocketbase-angular-app --standalone --routing --style=scss
cd pocketbase-angular-app
3. Integrating the PocketBase Client
Install the PocketBase JavaScript SDK in your Angular project. This SDK provides a convenient way to interact with your PocketBase backend.
npm install pocketbase
Create an Angular service to encapsulate PocketBase interactions. This promotes reusability and testability.
// src/app/core/services/pocketbase.service.ts
import { Injectable, signal } from '@angular/core';
import PocketBase from 'pocketbase';
import { Router } from '@angular/router';
import { environment } from '../../../environments/environment';
export interface Task {
id: string;
title: string;
isComplete: boolean;
user: string;
collectionId: string;
collectionName: string;
created: string;
updated: string;
}
@Injectable({ providedIn: 'root' })
export class PocketbaseService {
private pb = new PocketBase(environment.pocketbaseUrl);
currentUser = signal<any | null>(this.pb.authStore.model);
constructor(private router: Router) {
// React to auth store changes
this.pb.authStore.onChange(() => {
this.currentUser.set(this.pb.authStore.model);
if (!this.pb.authStore.isValid) {
this.router.navigate(['/login']);
}
}, true);
}
get client(): PocketBase {
return this.pb;
}
isLoggedIn(): boolean {
return this.pb.authStore.isValid;
}
async login(email: string, password: string): Promise<void> {
await this.pb.collection('users').authWithPassword(email, password);
this.router.navigate(['/tasks']);
}
async logout(): Promise<void> {
this.pb.authStore.clear();
this.router.navigate(['/login']);
}
async register(email: string, password: string): Promise<void> {
await this.pb.collection('users').create({ email, password, passwordConfirm: password });
await this.login(email, password);
}
async getTasks(): Promise<Task[]> {
if (!this.pb.authStore.isValid) return [];
return this.pb.collection('tasks').getFullList<Task>({
filter: `user = "${this.pb.authStore.model?.id}"`,
sort: '-created'
});
}
async addTask(title: string): Promise<Task> {
if (!this.pb.authStore.isValid) throw new Error('Not authenticated');
return this.pb.collection('tasks').create<Task>({
title,
isComplete: false,
user: this.pb.authStore.model?.id
});
}
async updateTask(id: string, isComplete: boolean): Promise<Task> {
if (!this.pb.authStore.isValid) throw new Error('Not authenticated');
return this.pb.collection('tasks').update<Task>(id, { isComplete });
}
}
Remember to define environment.pocketbaseUrl in src/environments/environment.ts (e.g., pocketbaseUrl: 'http://127.0.0.1:8090').
4. Authentication Flow
Create a login component (src/app/auth/login.component.ts) that leverages the PocketbaseService for user authentication. Angular's reactive forms are ideal for handling user input.
// src/app/auth/login.component.ts
import { Component } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { PocketbaseService } from '../core/services/pocketbase.service';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-login',
standalone: true,
imports: [ReactiveFormsModule, RouterLink],
template: `
<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
<input type="email" formControlName="email" placeholder="Email" />
<input type="password" formControlName="password" placeholder="Password" />
<button type="submit" [disabled]="loginForm.invalid">Login</button>
</form>
<p>Don't have an account? <a routerLink="/register">Register here</a></p>
`,
})
export class LoginComponent {
loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required],
});
constructor(private fb: FormBuilder, private pbService: PocketbaseService) {}
async onSubmit(): Promise<void> {
if (this.loginForm.valid) {
const { email, password } = this.loginForm.value;
try {
await this.pbService.login(email!, password!); // Non-null assertion for simplicity
} catch (error) {
console.error('Login failed:', error);
alert('Login failed. Please check your credentials.');
}
}
}
}
A similar RegisterComponent can be created, calling this.pbService.register().
5. Building a Task Management Feature
Once authenticated, users should be able to manage their tasks. Create a TasksComponent that uses the PocketbaseService to fetch, add, and update tasks.
// src/app/tasks/tasks.component.ts
import { Component, OnInit } from '@angular/core';
import { PocketbaseService, Task } from '../core/services/pocketbase.service';
import { CommonModule } from '@angular/common';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
@Component({
selector: 'app-tasks',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<h2>My Tasks</h2>
<form [formGroup]="taskForm" (ngSubmit)="addTask()">
<input type="text" formControlName="title" placeholder="New task title" />
<button type="submit" [disabled]="taskForm.invalid">Add Task</button>
</form>
<ul>
<li *ngFor="let task of tasks">
<input
type="checkbox"
[checked]="task.isComplete"
(change)="toggleTaskCompletion(task.id, $event)"
/>
<span [class.completed]="task.isComplete">{{ task.title }}</span>
</li>
</ul>
<button (click)="pbService.logout()">Logout</button>
`,
styles: [`
.completed { text-decoration: line-through; color: #888; }
`]
})
export class TasksComponent implements OnInit {
tasks: Task[] = [];
taskForm = this.fb.group({
title: ['', Validators.required],
});
constructor(public pbService: PocketbaseService, private fb: FormBuilder) {}
ngOnInit(): void {
this.loadTasks();
}
async loadTasks(): Promise<void> {
try {
this.tasks = await this.pbService.getTasks();
} catch (error) {
console.error('Failed to load tasks:', error);
}
}
async addTask(): Promise<void> {
if (this.taskForm.valid) {
const title = this.taskForm.value.title!;
try {
const newTask = await this.pbService.addTask(title);
this.tasks.unshift(newTask); // Add to the beginning
this.taskForm.reset();
} catch (error) {
console.error('Failed to add task:', error);
}
}
}
async toggleTaskCompletion(taskId: string, event: Event): Promise<void> {
const isComplete = (event.target as HTMLInputElement).checked;
try {
const updatedTask = await this.pbService.updateTask(taskId, isComplete);
const index = this.tasks.findIndex(t => t.id === taskId);
if (index !== -1) {
this.tasks[index] = updatedTask;
}
} catch (error) {
console.error('Failed to update task:', error);
}
}
}
Finally, configure your Angular router (src/app/app.routes.ts) to include these components and protect routes with a simple guard.
// src/app/app.routes.ts
import { Routes } from '@angular/router';
import { LoginComponent } from './auth/login.component';
import { RegisterComponent } from './auth/register.component'; // Assume you created this
import { TasksComponent } from './tasks/tasks.component';
import { inject } from '@angular/core';
import { PocketbaseService } from './core/services/pocketbase.service';
const authGuard = () => {
const pbService = inject(PocketbaseService);
return pbService.isLoggedIn() ? true : pbService.router.createUrlTree(['/login']);
};
export const routes: Routes = [
{ path: 'login', component: LoginComponent },
{ path: 'register', component: RegisterComponent },
{ path: 'tasks', component: TasksComponent, canActivate: [authGuard] },
{ path: '', redirectTo: '/tasks', pathMatch: 'full' },
{ path: '**', redirectTo: '/tasks' },
];
Conclusion
Integrating Angular with PocketBase offers an incredibly efficient way to develop full-stack applications. Angular's structured component-based architecture and PocketBase's all-in-one backend solution reduce boilerplate and accelerate development. This combination is particularly well-suited for prototypes, MVPs, and even production applications where a self-hosted, lightweight backend is preferred. Explore PocketBase's real-time subscriptions and file storage capabilities to further enhance your applications.