NX monorepo with Next.js 16 (App Router), Yarn 4, Keycloak auth via NextAuth.js, Digdir Designsystemet UI.
yarn start <app> # Dev server (concept-catalog, service-catalog, dataset-catalog, data-service-catalog)
yarn build <app> # Build specific app
yarn lint / yarn lint-all # Lint affected / all
yarn test / yarn test-all # Test affected / all
yarn nx e2e <app>-e2e # E2E tests (add --ui --debug for dev)
yarn graphql:codegen # Regenerate GraphQL types after changing .graphql filesapps/
concept-catalog, service-catalog, dataset-catalog, data-service-catalog/ # Next.js apps
catalog-portal, catalog-admin/ # Portal and admin
*-e2e/ # Playwright E2E tests
libs/
data-access/ # API clients (libs/data-access/src/lib/<domain>/api/index.ts)
ui/ # Shared components (wrap Digdir Designsystemet)
types/ # Shared TypeScript types
utils/ # Auth, localization, validation
import { ... } from '@catalog-frontend/data-access'; // API functions
import { ... } from '@catalog-frontend/ui'; // UI components
import { ... } from '@catalog-frontend/types'; // TypeScript types
import { ... } from '@catalog-frontend/utils'; // Utilities, localization
import { ... } from '@concept-catalog/...'; // App-specific importsEach app: app/ (layout, actions/, api/, catalogs/[catalogId]/), components/, hooks/, utils/auth.ts
const MyPage = withReadProtectedPage(
({ catalogId }) => `/catalogs/${catalogId}/path`,
async ({ catalogId, session, hasWritePermission, hasAdminPermission }) => <MyPageClient {...props} />,
);"use server";
export async function updateConcept(initialConcept, values) {
const session = await getValidSession();
if (!session) return redirectToSignIn();
const diff = compare(initialConcept, values); // JSON Patch via fast-json-patch
await patchConceptApi(id, diff, session.accessToken);
updateTag("concepts"); // Always invalidate cache after mutations
}Component → useQuery (hooks/) → API Route (app/api/) → Data-Access (libs/data-access) → Backend
Data-access: accept accessToken, validate inputs (validateUUID, validateOrganizationNumber), return raw Response.
const session = await getValidSession(); // Server components/actions
const session = await getServerSession(authOptions); // API routes
hasOrganizationReadPermission(token, catalogId); // Permission checks (also Write/Admin)Single language (Norwegian Bokmål). Use localization from @catalog-frontend/utils:
localization.catalogType.concept, localization.alert.fail, localization.conceptForm.fieldName
- CSS/SCSS Modules, design tokens:
var(--fds-spacing-2) - Compound components:
FormLayout.Section,InfoCard.Item - Formik:
useFormikContext<FormType>() - New:
libs/ui/src/lib/<name>/index.tsx+.module.css, export inlibs/ui/src/index.ts
- Params are Promises:
const { catalogId } = await params;(Next.js 15+) - forwardRef for inputs: Use
forwardRefwithdisplayNamefor form components - Validation before API calls: Always validate UUIDs and org numbers
- Error messages: Use
localization.alert.*, never hardcoded text - Cache invalidation: Call
updateTag()after every mutation - Spread props: Components accept and spread additional props
corepack enable && yarn && yarn start concept-catalog # http://localhost:4200Catalog apps need catalog ID in URL. If redirected to portal, select a catalog and replace domain with localhost:4200.
Hard rules (do not violate):
- Never use
any- Model types properly. Prefer generics, unions, orunknown+ narrowing. - Do not hide type errors - Avoid
@ts-ignore,@ts-nocheck, broad ESLint disables. If exception required, document why and add removal condition. - No unsafe type assertions - Avoid
as SomeTypeunless at a trusted boundary with justifying comment.
- Always write Playwright tests idiomatically, following best practices with clear structure, reliable selectors, and user-focused assertions