| name | nextcloud-impl-app-development |
| description | Use when building a complete Nextcloud app, connecting PHP backend to Vue.js frontend, or implementing CRUD operations. Prevents skipping the service layer, tight coupling between controllers and database, and missing initial state bridge. Covers full-stack Nextcloud app development workflow including creating controllers with routes, implementing service layer with DI, database entities and mappers, Vue.js frontend with @nextcloud packages, initial state bridge between PHP and JavaScript, and the development lifecycle. Keywords: full-stack, CRUD, service layer, initial state, routes.php, Vue.js, controller, mapper, build full app, connect frontend to backend, CRUD operations, full-stack tutorial..
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Nextcloud 28+. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
nextcloud-impl-app-development
Quick Reference
Full-Stack App Layer Map
| Layer | PHP (Backend) | Vue.js (Frontend) |
|---|
| Entry point | lib/AppInfo/Application.php | src/main.js |
| Routing | appinfo/routes.php | @nextcloud/router |
| Controllers | lib/Controller/*.php | N/A |
| Services | lib/Service/*.php | src/services/*.js |
| Data access | lib/Db/Entity.php + Mapper.php | @nextcloud/axios |
| State bridge | IInitialState::provideInitialState() | loadState() |
| UI components | N/A | @nextcloud/vue |
| Notifications | N/A | @nextcloud/dialogs |
Development Commands
| Command | Purpose |
|---|
npm run dev | Build frontend for development |
npm run build | Build frontend for production |
npm run watch | Rebuild on file changes |
npm run serve | Dev server with HMR |
php occ app:enable myapp | Enable the app |
php occ app:disable myapp | Disable the app |
php occ migrations:migrate myapp | Run database migrations |
Critical Warnings
ALWAYS set <namespace> in appinfo/info.xml -- auto-wiring and autoloading depend on it.
ALWAYS use @nextcloud/axios for HTTP requests -- it handles CSRF tokens and authentication automatically.
ALWAYS use provideInitialState() in PHP and loadState() in JS for server-to-client data -- NEVER inject data via inline scripts or global variables.
ALWAYS use CSS custom properties (--color-*) for colors -- NEVER hardcode colors (breaks dark mode and theming).
ALWAYS use direct component imports (@nextcloud/vue/components/NcButton) -- barrel imports increase bundle size.
NEVER use \OCP\Server::get() for service resolution -- use constructor injection for testability.
NEVER use OCP\ILogger -- deprecated since NC 24. Use Psr\Log\LoggerInterface.
NEVER use raw fetch() or plain axios -- use @nextcloud/axios which handles auth headers automatically.
NEVER call loadState() without a fallback for optional data -- it throws on missing keys.
NEVER modify existing migration files -- create new migrations for schema changes.
Decision Tree: Route Type Selection
Is this endpoint consumed by external clients or other apps?
├── YES: Will responses need the OCS JSON/XML envelope?
│ ├── YES → Use OCS route + OCSController
│ │ Route: 'ocs' => [['name' => 'api#method', 'url' => '/api/v1/...']]
│ └── NO → Use regular route + ApiController (adds CORS)
│ Route: 'routes' => [['name' => 'api#method', 'url' => '/api/...']]
└── NO: Internal app use only
├── Page rendering? → Use Controller + TemplateResponse
│ Route: 'routes' => [['name' => 'page#index', 'url' => '/']]
├── CRUD resource? → Use resource routes
│ Route: 'resources' => ['item' => ['url' => '/items']]
└── AJAX from Vue frontend? → Use regular route + JSONResponse
Route: 'routes' => [['name' => 'item#create', 'url' => '/items']]
Essential Patterns
Pattern 1: Full-Stack App Creation (Step-by-Step)
Step 1: App manifest (appinfo/info.xml)
<?xml version="1.0"?>
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://apps.nextcloud.com/schema/apps/info.xsd">
<id>taskboard</id>
<name>Task Board</name>
<summary>Simple task management</summary>
<description>A task board for managing project tasks</description>
<version>1.0.0</version>
<licence>AGPL-3.0-or-later</licence>
<author>Developer Name</author>
<namespace>TaskBoard</namespace>
<category>organization</category>
<dependencies>
<nextcloud min-version="28" max-version="32"/>
</dependencies>
<navigations>
<navigation>
Task Board
taskboard.page.index
app.svg
Step 2: Application bootstrap (lib/AppInfo/Application.php)
<?php
namespace OCA\TaskBoard\AppInfo;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
class Application extends App implements IBootstrap {
public const APP_ID = 'taskboard';
public function __construct() {
parent::__construct(self::APP_ID);
}
public function register(IRegistrationContext $context): void {
}
public function boot(): {
}
}
Step 3: Routes (appinfo/routes.php)
<?php
return [
'routes' => [
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
],
'ocs' => [
['name' => 'task_api#index', 'url' => '/api/v1/tasks', 'verb' => 'GET'],
['name' => 'task_api#show', 'url' => '/api/v1/tasks/{id}', 'verb' => 'GET'],
['name' => 'task_api#create', 'url' => '/api/v1/tasks', 'verb' => 'POST'],
['name' => 'task_api#update', 'url' => '/api/v1/tasks/{id}', 'verb' => 'PUT'],
['name' => 'task_api#destroy', 'url' => '/api/v1/tasks/{id}', 'verb' => 'DELETE'],
],
];
Step 4-8: See references/examples.md for the complete Entity, Mapper, Service, Controller, and Vue.js implementation.
Pattern 2: Initial State Bridge (PHP to JavaScript)
PHP side -- provide data in controller:
use OCP\AppFramework\Services\IInitialState;
class PageController extends Controller {
public function __construct(
string $appName,
IRequest $request,
private IInitialState $initialState,
private TaskService $service,
private ?string $userId,
) {
parent::__construct($appName, $request);
}
#[NoAdminRequired]
#[NoCSRFRequired]
public function index(): TemplateResponse {
$this->initialState->provideInitialState(
'tasks',
$this->service->findAll($this->userId)
);
$this->initialState->provideLazyInitialState(
'config',
fn () => ->service->()
);
(, );
}
}
JavaScript side -- consume data:
import { loadState } from '@nextcloud/initial-state'
const tasks = loadState('taskboard', 'tasks', [])
const config = loadState('taskboard', 'config', { maxTasks: 100 })
Pattern 3: Frontend API Service Layer
import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
const baseUrl = generateOcsUrl('/apps/taskboard/api/v1')
export async function fetchTasks() {
const response = await axios.get(`${baseUrl}/tasks`)
return response.data.ocs.data
}
export async function createTask(title, description) {
const response = await axios.post(`${baseUrl}/tasks`, { title, description })
return response.data.ocs.data
}
export async function updateTask(id, data) {
const response = await axios.put(`${baseUrl}/tasks/${id}`, data)
return response.data.ocs.
}
() {
axios.()
}
Pattern 4: Vue.js App Entry Point
import Vue from 'vue'
import App from './App.vue'
const appElement = document.getElementById('content')
new Vue({
el: appElement,
render: h => h(App),
})
<?php
script('taskboard', 'taskboard-main');
style('taskboard', 'main');
?>
<div id="content"></div>
Pattern 5: Vue Component with Nextcloud UI
<!-- src/App.vue -->
<template>
<NcContent app-name="taskboard">
<NcAppNavigation>
<NcAppNavigationItem v-for="task in tasks"
:key="task.id"
:name="task.title"
@click="selectTask(task)" />
</NcAppNavigation>
<NcAppContent>
<NcEmptyContent v-if="!selectedTask"
name="No task selected"
description="Select a task from the sidebar">
</NcEmptyContent>
<div v-else class="task-detail">
<h2>{{ selectedTask.title }}</h2>
<p>{{ selectedTask.description }}</p>
</div>
</NcAppContent>
</NcContent>
</template>
<script>
import NcContent from '@nextcloud/vue/components/NcContent'
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
import NcAppNavigation from '@nextcloud/vue/components/NcAppNavigation'
import NcAppNavigationItem from '@nextcloud/vue/components/NcAppNavigationItem'
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
import { loadState } from '@nextcloud/initial-state'
import { showSuccess, showError } from '@nextcloud/dialogs'
import '@nextcloud/dialogs/style.css'
import { fetchTasks, deleteTask } from './services/TaskService'
export default {
name: 'App',
components: {
NcContent,
NcAppContent,
NcAppNavigation,
NcAppNavigationItem,
NcEmptyContent,
},
data() {
return {
tasks: loadState('taskboard', 'tasks', []),
selectedTask: null,
}
},
methods: {
selectTask(task) {
this.selectedTask = task
},
},
}
</script>
<style scoped>
.task-detail {
padding: 20px;
color: var(--color-main-text);
}
</style>
Pattern 6: Webpack Configuration
const webpackConfig = require('@nextcloud/webpack-vue-config')
module.exports = webpackConfig
{
"scripts": {
"build": "webpack --node-env production --progress",
"dev": "webpack --node-env development --progress",
"watch": "webpack --node-env development --progress --watch",
"serve": "webpack --node-env development serve --progress"
},
"dependencies": {
"@nextcloud/axios": "^2.0.0",
"@nextcloud/dialogs": "^5.0.0",
"@nextcloud/initial-state": "^2.0.0",
"@nextcloud/router": "^3.0.0",
"@nextcloud/vue": "^8.0.0",
"vue": "^2.7.0"
},
"devDependencies": {
Development Lifecycle
New App Checklist
- Scaffold -- Use https://apps.nextcloud.com/developer/apps/generate or create manually
- Configure -- Set
info.xml with correct <namespace>, <dependencies>, <navigations>
- Backend -- Create Entity, Mapper, Service, Controller chain
- Database -- Create migration in
lib/Migration/
- Routes -- Define in
appinfo/routes.php
- Frontend -- Set up
src/main.js, App.vue, webpack config
- Bridge -- Connect PHP and JS via
IInitialState / loadState()
- Build -- Run
npm install && npm run build
- Enable -- Run
php occ app:enable myapp
- Test -- Verify in browser, check browser console and Nextcloud log
File Naming Conventions
| Layer | Convention | Example |
|---|
| Entity | Singular PascalCase | lib/Db/Task.php |
| Mapper | Entity + Mapper | lib/Db/TaskMapper.php |
| Service | Entity + Service | lib/Service/TaskService.php |
| Controller | Entity + Controller or ApiController | lib/Controller/TaskApiController.php |
| Migration | Version{MMDD}Date{timestamp} | lib/Migration/Version1000Date20240101000000.php |
| Vue component | PascalCase .vue | src/components/TaskItem.vue |
| JS service | PascalCase .js | src/services/TaskService.js |
Reference Links
Official Sources