diff --git a/typescript/rest-analog/.editorconfig b/typescript/rest-analog/.editorconfig
new file mode 100644
index 000000000000..59d9a3a3e73f
--- /dev/null
+++ b/typescript/rest-analog/.editorconfig
@@ -0,0 +1,16 @@
+# Editor configuration, see https://editorconfig.org
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.ts]
+quote_type = single
+
+[*.md]
+max_line_length = off
+trim_trailing_whitespace = false
diff --git a/typescript/rest-analog/.gitignore b/typescript/rest-analog/.gitignore
new file mode 100644
index 000000000000..bf149eb7cc0e
--- /dev/null
+++ b/typescript/rest-analog/.gitignore
@@ -0,0 +1,44 @@
+# See http://help.github.com/ignore-files/ for more about ignoring files.
+
+# Compiled output
+/dist
+/tmp
+/out-tsc
+/bazel-out
+
+# Node
+/node_modules
+npm-debug.log
+yarn-error.log
+
+# IDEs and editors
+.idea/
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# Visual Studio Code
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+.history/*
+
+# Miscellaneous
+/.angular/cache
+/.nx/cache
+/.nx/workspace-data
+.sass-cache/
+/connect.lock
+/coverage
+/libpeerconnection.log
+testem.log
+/typings
+
+# System files
+.DS_Store
+Thumbs.db
diff --git a/typescript/rest-analog/README.md b/typescript/rest-analog/README.md
new file mode 100644
index 000000000000..a49027b883c6
--- /dev/null
+++ b/typescript/rest-analog/README.md
@@ -0,0 +1,291 @@
+# Fullstack Example with Analog (REST API)
+
+This example shows how to implement a **fullstack app with [Analog](https://analogjs.org/)** using [Prisma Client](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client). It uses a SQLite database file with some initial dummy data which you can find at [`./prisma/dev.db`](./prisma/dev.db).
+
+## Getting started
+
+### 1. Download example and install dependencies
+
+Download this example:
+
+```
+npx try-prisma@latest --template typescript/rest-analog
+```
+
+Install npm dependencies:
+
+```
+cd rest-analog
+npm install
+```
+
+Alternative: Clone the entire repo
+
+Clone this repository:
+
+```
+git clone git@github.com:prisma/prisma-examples.git --depth=1
+```
+
+Install npm dependencies:
+
+```
+cd prisma-examples/typescript/rest-analog
+npm install
+```
+
+
+
+### 2. Create and seed the database
+
+Run the following command to create your SQLite database file. This also creates the `User` and `Post` tables that are defined in [`prisma/schema.prisma`](./prisma/schema.prisma):
+
+```
+npx prisma migrate dev --name init
+```
+
+When `npx prisma migrate dev` is executed against a newly created database, seeding is also triggered. The seed file in [`prisma/seed.ts`](./prisma/seed.ts) will be executed and your database will be populated with the sample data.
+
+### 3. Start the app
+
+```
+npm run dev
+```
+
+The app is now running, navigate to [`http://localhost:5173/`](http://localhost:5173/) in your browser to explore its UI.
+
+Expand for a tour through the UI of the app
+
+
+
+**Blog** (located in [`./src/app/pages/index.page.ts`](./src/app/pages/index.page.ts)
+
+![](https://imgur.com/eepbOUO.png)
+
+**Signup** (located in [`./rc/app/pages/signup.page.ts`](./src/app/pages/signup.page.ts))
+
+![](https://imgur.com/iE6OaBI.png)
+
+**Create post (draft)** (located in [`./src/app/pages/create.page.ts`](./src/app/pages/create.page.ts))
+
+![](https://imgur.com/olCWRNv.png)
+
+**Drafts** (located in [`./src/app/pages/drafts.page.ts`](./src/app/pages/drafts.page.ts))
+
+![](https://imgur.com/PSMzhcd.png)
+
+**View post** (located in [`./src/app/pages/p/_id.vue`](src/app/pages/post/_id.vue)) (delete or publish here)
+
+![](https://imgur.com/zS1B11O.png)
+
+
+
+## Using the REST API
+
+You can also access the REST API of the API server directly. It is running on the same host machine and port and can be accessed via the `/api` route (in this case that is `localhost:3000/api/`, so you can e.g. reach the API with [`localhost:3000/api/feed`](http://localhost:3000/api/feed)).
+
+### `GET`
+
+- `/api/post/:id`: Fetch a single post by its `id`
+- `/api/feed`: Fetch all _published_ posts
+- `/api/filterPosts?searchString={searchString}`: Filter posts by `title` or `content`
+
+### `POST`
+
+- `/api/post`: Create a new post
+ - Body:
+ - `title: String` (required): The title of the post
+ - `content: String` (optional): The content of the post
+ - `authorEmail: String` (required): The email of the user that creates the post
+- `/api/user`: Create a new user
+ - Body:
+ - `email: String` (required): The email address of the user
+ - `name: String` (optional): The name of the user
+
+### `PUT`
+
+- `/api/publish/:id`: Publish a post by its `id`
+
+### `DELETE`
+
+- `/api/post/:id`: Delete a post by its `id`
+
+## Switch to another database (e.g. PostgreSQL, MySQL, SQL Server, MongoDB)
+
+If you want to try this example with another database than SQLite, you can adjust the the database connection in [`prisma/schema.prisma`](./prisma/schema.prisma) by reconfiguring the `datasource` block.
+
+Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls).
+
+Expand for an overview of example configurations with different databases
+
+### PostgreSQL
+
+For PostgreSQL, the connection URL has the following structure:
+
+```prisma
+datasource db {
+ provider = "postgresql"
+ url = "postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA"
+}
+```
+
+Here is an example connection string with a local PostgreSQL database:
+
+```prisma
+datasource db {
+ provider = "postgresql"
+ url = "postgresql://janedoe:mypassword@localhost:5432/notesapi?schema=public"
+}
+```
+
+### MySQL
+
+For MySQL, the connection URL has the following structure:
+
+```prisma
+datasource db {
+ provider = "mysql"
+ url = "mysql://USER:PASSWORD@HOST:PORT/DATABASE"
+}
+```
+
+Here is an example connection string with a local MySQL database:
+
+```prisma
+datasource db {
+ provider = "mysql"
+ url = "mysql://janedoe:mypassword@localhost:3306/notesapi"
+}
+```
+
+### Microsoft SQL Server
+
+Here is an example connection string with a local Microsoft SQL Server database:
+
+```prisma
+datasource db {
+ provider = "sqlserver"
+ url = "sqlserver://localhost:1433;initial catalog=sample;user=sa;password=mypassword;"
+}
+```
+
+### MongoDB
+
+Here is an example connection string with a local MongoDB database:
+
+```prisma
+datasource db {
+ provider = "mongodb"
+ url = "mongodb://USERNAME:PASSWORD@HOST/DATABASE?authSource=admin&retryWrites=true&w=majority"
+}
+```
+
+
+
+## Evolving the app
+
+Evolving the application typically requires three steps:
+
+1. Migrate your database using Prisma Migrate
+1. Update your server-side application code
+1. Build new UI features in Vue
+
+For the following example scenario, assume you want to add a "profile" feature to the app where users can create a profile and write a short bio about themselves.
+
+### 1. Migrate your database using Prisma Migrate
+
+The first step is to add a new table, e.g. called `Profile`, to the database. You can do this by adding a new model to your [Prisma schema file](./prisma/schema.prisma) file and then running a migration afterwards:
+
+```diff
+// schema.prisma
+
+model Post {
+ id Int @default(autoincrement()) @id
+ title String
+ content String?
+ published Boolean @default(false)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int
+}
+
+model User {
+ id Int @default(autoincrement()) @id
+ name String?
+ email String @unique
+ posts Post[]
++ profile Profile?
+}
+
++model Profile {
++ id Int @default(autoincrement()) @id
++ bio String?
++ userId Int @unique
++ user User @relation(fields: [userId], references: [id])
++}
+```
+
+Once you've updated your data model, you can execute the changes against your database with the following command:
+
+```
+npx prisma migrate dev --name add-profile
+```
+
+### 2. Update your application code
+
+You can now use your `PrismaClient` instance to perform operations against the new `Profile` table. Here are some examples:
+
+#### Create a new profile for an existing user
+
+```ts
+const profile = await prisma.profile.create({
+ data: {
+ bio: 'Hello World',
+ user: {
+ connect: { email: 'alice@prisma.io' },
+ },
+ },
+})
+```
+
+#### Create a new user with a new profile
+
+```ts
+const user = await prisma.user.create({
+ data: {
+ email: 'john@prisma.io',
+ name: 'John',
+ profile: {
+ create: {
+ bio: 'Hello World',
+ },
+ },
+ },
+})
+```
+
+#### Update the profile of an existing user
+
+```ts
+const userWithUpdatedProfile = await prisma.user.update({
+ where: { email: 'alice@prisma.io' },
+ data: {
+ profile: {
+ update: {
+ bio: 'Hello Friends',
+ },
+ },
+ },
+})
+```
+
+### 5. Build new UI features in Vue
+
+Once you have added a new endpoint to the API (e.g. `/api/profile` with `/POST`, `/PUT` and `GET` operations), you can start building a new UI component in Vue. It could e.g. be called `profile.vue` and would be located in the `pages` directory.
+
+In the application code, you can access the new endpoint via `fetch` operations and populate the UI with the data you receive from the API calls.
+
+## Next steps
+
+- Check out the [Prisma docs](https://www.prisma.io/docs)
+- Share your feedback on the [Prisma Discord](https://pris.ly/discord/)
+- Create issues and ask questions on [GitHub](https://github.com/prisma/prisma/)
diff --git a/typescript/rest-analog/angular.json b/typescript/rest-analog/angular.json
new file mode 100644
index 000000000000..d8c37010b799
--- /dev/null
+++ b/typescript/rest-analog/angular.json
@@ -0,0 +1,57 @@
+{
+ "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
+ "version": 1,
+ "newProjectRoot": "projects",
+ "projects": {
+ "my-app": {
+ "projectType": "application",
+ "root": ".",
+ "sourceRoot": "src",
+ "prefix": "app",
+ "architect": {
+ "build": {
+ "builder": "@analogjs/platform:vite",
+ "options": {
+ "configFile": "vite.config.ts",
+ "main": "src/main.ts",
+ "outputPath": "dist/client",
+ "tsConfig": "tsconfig.app.json"
+ },
+ "defaultConfiguration": "production",
+ "configurations": {
+ "development": {
+ "mode": "development"
+ },
+ "production": {
+ "sourcemap": false,
+ "mode": "production"
+ }
+ }
+ },
+ "serve": {
+ "builder": "@analogjs/platform:vite-dev-server",
+ "defaultConfiguration": "development",
+ "options": {
+ "buildTarget": "my-app:build",
+ "port": 5173
+ },
+ "configurations": {
+ "development": {
+ "buildTarget": "my-app:build:development",
+ "hmr": true
+ },
+ "production": {
+ "buildTarget": "my-app:build:production"
+ }
+ }
+ },
+ "test": {
+ "builder": "@analogjs/vitest-angular:test"
+ }
+ }
+ }
+ },
+ "cli": {
+ "analytics": false
+ }
+}
diff --git a/typescript/rest-analog/index.html b/typescript/rest-analog/index.html
new file mode 100644
index 000000000000..8cdd0748c8fc
--- /dev/null
+++ b/typescript/rest-analog/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+ My App
+
+
+
+
+
+
+
+
+
+
diff --git a/typescript/rest-analog/package.json b/typescript/rest-analog/package.json
new file mode 100644
index 000000000000..8038436ab935
--- /dev/null
+++ b/typescript/rest-analog/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "rest-analog",
+ "version": "0.0.0",
+ "type": "module",
+ "engines": {
+ "node": ">=18.19.1"
+ },
+ "scripts": {
+ "ng": "ng",
+ "dev": "ng serve",
+ "start": "pnpm run dev",
+ "build": "ng build",
+ "watch": "ng build --watch --configuration development",
+ "test": "ng test"
+ },
+ "private": true,
+ "dependencies": {
+ "@analogjs/content": "^1.7.1",
+ "@analogjs/router": "^1.7.1",
+ "@angular/animations": "^18.0.0",
+ "@angular/common": "^18.0.0",
+ "@angular/compiler": "^18.0.0",
+ "@angular/core": "^18.0.0",
+ "@angular/forms": "^18.0.0",
+ "@angular/platform-browser": "^18.0.0",
+ "@angular/platform-browser-dynamic": "^18.0.0",
+ "@angular/platform-server": "^18.0.0",
+ "@angular/router": "^18.0.0",
+ "@prisma/client": "^5.18.0",
+ "front-matter": "^4.0.2",
+ "marked": "^5.0.2",
+ "marked-gfm-heading-id": "^3.1.0",
+ "marked-highlight": "^2.0.1",
+ "marked-mangle": "^1.1.7",
+ "prismjs": "^1.29.0",
+ "rxjs": "~7.8.0",
+ "tslib": "^2.3.0",
+ "zone.js": "~0.14.3"
+ },
+ "devDependencies": {
+ "@analogjs/platform": "^1.7.1",
+ "@analogjs/vite-plugin-angular": "^1.7.1",
+ "@analogjs/vitest-angular": "^1.7.1",
+ "@angular-devkit/build-angular": "^18.0.0",
+ "@angular/cli": "^18.0.0",
+ "@angular/compiler-cli": "^18.0.0",
+ "jsdom": "^22.0.0",
+ "prisma": "^5.18.0",
+ "typescript": "~5.4.2",
+ "vite": "^5.0.0",
+ "vite-tsconfig-paths": "^4.2.0",
+ "vitest": "^1.3.1"
+ },
+ "prisma": {
+ "seed": "node prisma/seed.js"
+ }
+}
diff --git a/typescript/rest-analog/prisma/db.ts b/typescript/rest-analog/prisma/db.ts
new file mode 100644
index 000000000000..a356dd972dce
--- /dev/null
+++ b/typescript/rest-analog/prisma/db.ts
@@ -0,0 +1,16 @@
+// https://www.prisma.io/docs/guides/database/troubleshooting-orm/help-articles/nextjs-prisma-client-dev-practices
+import { PrismaClient } from '@prisma/client'
+
+const prismaClientSingleton = () => {
+ return new PrismaClient()
+}
+
+type PrismaClientSingleton = ReturnType
+
+const globalForPrisma = globalThis as unknown as {
+ prisma: PrismaClientSingleton | undefined
+}
+
+export const prisma = globalForPrisma.prisma ?? prismaClientSingleton()
+
+if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
diff --git a/typescript/rest-analog/prisma/schema.prisma b/typescript/rest-analog/prisma/schema.prisma
new file mode 100644
index 000000000000..0c02acd6eb76
--- /dev/null
+++ b/typescript/rest-analog/prisma/schema.prisma
@@ -0,0 +1,27 @@
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "sqlite"
+ url = "file:./dev.db"
+}
+
+model User {
+ id Int @id @default(autoincrement())
+ email String @unique
+ name String?
+ posts Post[]
+}
+
+model Post {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ title String
+ content String?
+ published Boolean @default(false)
+ viewCount Int @default(0)
+ author User? @relation(fields: [authorId], references: [id])
+ authorId Int?
+}
diff --git a/typescript/rest-analog/prisma/seed.js b/typescript/rest-analog/prisma/seed.js
new file mode 100644
index 000000000000..50448b28efe6
--- /dev/null
+++ b/typescript/rest-analog/prisma/seed.js
@@ -0,0 +1,72 @@
+import { PrismaClient } from '@prisma/client'
+
+const prisma = new PrismaClient()
+
+const userData = [
+ {
+ name: 'Alice',
+ email: 'alice@prisma.io',
+ posts: {
+ create: [
+ {
+ title: 'Join the Prisma Discord',
+ content: 'https://pris.ly/discord',
+ published: true,
+ },
+ ],
+ },
+ },
+ {
+ name: 'Nilu',
+ email: 'nilu@prisma.io',
+ posts: {
+ create: [
+ {
+ title: 'Follow Prisma on Twitter',
+ content: 'https://www.twitter.com/prisma',
+ published: true,
+ viewCount: 42,
+ },
+ ],
+ },
+ },
+ {
+ name: 'Mahmoud',
+ email: 'mahmoud@prisma.io',
+ posts: {
+ create: [
+ {
+ title: 'Ask a question about Prisma on GitHub',
+ content: 'https://www.github.com/prisma/prisma/discussions',
+ published: true,
+ viewCount: 128,
+ },
+ {
+ title: 'Prisma on YouTube',
+ content: 'https://pris.ly/youtube',
+ },
+ ],
+ },
+ },
+]
+
+async function main() {
+ console.log(`Start seeding ...`)
+ for (const u of userData) {
+ const user = await prisma.user.create({
+ data: u,
+ })
+ console.log(`Created user with id: ${user.id}`)
+ }
+ console.log(`Seeding finished.`)
+}
+
+main()
+ .then(async () => {
+ await prisma.$disconnect()
+ })
+ .catch(async (e) => {
+ console.error(e)
+ await prisma.$disconnect()
+ process.exit(1)
+ })
diff --git a/typescript/rest-analog/public/.gitkeep b/typescript/rest-analog/public/.gitkeep
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/typescript/rest-analog/public/favicon.ico b/typescript/rest-analog/public/favicon.ico
new file mode 100644
index 000000000000..997406ad22c2
Binary files /dev/null and b/typescript/rest-analog/public/favicon.ico differ
diff --git a/typescript/rest-analog/src/app/app.analog b/typescript/rest-analog/src/app/app.analog
new file mode 100644
index 000000000000..9e33f440d901
--- /dev/null
+++ b/typescript/rest-analog/src/app/app.analog
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/typescript/rest-analog/src/app/app.config.server.ts b/typescript/rest-analog/src/app/app.config.server.ts
new file mode 100644
index 000000000000..a8002f197f6d
--- /dev/null
+++ b/typescript/rest-analog/src/app/app.config.server.ts
@@ -0,0 +1,10 @@
+import { mergeApplicationConfig, ApplicationConfig } from '@angular/core'
+import { provideServerRendering } from '@angular/platform-server'
+
+import { appConfig } from './app.config'
+
+const serverConfig: ApplicationConfig = {
+ providers: [provideServerRendering()],
+}
+
+export const config = mergeApplicationConfig(appConfig, serverConfig)
diff --git a/typescript/rest-analog/src/app/app.config.ts b/typescript/rest-analog/src/app/app.config.ts
new file mode 100644
index 000000000000..cd3473969270
--- /dev/null
+++ b/typescript/rest-analog/src/app/app.config.ts
@@ -0,0 +1,20 @@
+import {
+ provideHttpClient,
+ withFetch,
+ withInterceptors,
+} from '@angular/common/http'
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core'
+import { provideClientHydration } from '@angular/platform-browser'
+import { provideFileRouter, requestContextInterceptor } from '@analogjs/router'
+
+export const appConfig: ApplicationConfig = {
+ providers: [
+ provideZoneChangeDetection({ eventCoalescing: true }),
+ provideFileRouter(),
+ provideHttpClient(
+ withFetch(),
+ withInterceptors([requestContextInterceptor]),
+ ),
+ provideClientHydration(),
+ ],
+}
diff --git a/typescript/rest-analog/src/app/components/header.analog b/typescript/rest-analog/src/app/components/header.analog
new file mode 100644
index 000000000000..51904183c9cf
--- /dev/null
+++ b/typescript/rest-analog/src/app/components/header.analog
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/typescript/rest-analog/src/app/components/post/post.component.css b/typescript/rest-analog/src/app/components/post/post.component.css
new file mode 100644
index 000000000000..11662f669ff3
--- /dev/null
+++ b/typescript/rest-analog/src/app/components/post/post.component.css
@@ -0,0 +1,13 @@
+:host {
+ display: block;
+ padding: 1rem;
+ background: white;
+}
+
+h2 {
+ margin-block: 0;
+}
+
+:host:hover {
+ box-shadow: 1px 1px 3px #aaa;
+}
diff --git a/typescript/rest-analog/src/app/components/post/post.component.html b/typescript/rest-analog/src/app/components/post/post.component.html
new file mode 100644
index 000000000000..bb6ee3b40e64
--- /dev/null
+++ b/typescript/rest-analog/src/app/components/post/post.component.html
@@ -0,0 +1,3 @@
+{{ post().title }}
+By {{ post().author.name }}
+
diff --git a/typescript/rest-analog/src/app/components/post/post.component.spec.ts b/typescript/rest-analog/src/app/components/post/post.component.spec.ts
new file mode 100644
index 000000000000..9884c7a36424
--- /dev/null
+++ b/typescript/rest-analog/src/app/components/post/post.component.spec.ts
@@ -0,0 +1,31 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing'
+
+import { PostComponent } from './post.component'
+
+describe('PostComponent', () => {
+ let component: PostComponent
+ let fixture: ComponentFixture
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ imports: [PostComponent],
+ }).compileComponents()
+
+ fixture = TestBed.createComponent(PostComponent)
+ component = fixture.componentInstance
+ })
+
+ it('should create', () => {
+ fixture.componentRef.setInput('post', {
+ id: 1,
+ title: 'title',
+ content: 'content',
+ author: {
+ name: 'Gerome',
+ },
+ })
+
+ fixture.detectChanges()
+ expect(component).toBeTruthy()
+ })
+})
diff --git a/typescript/rest-analog/src/app/components/post/post.component.ts b/typescript/rest-analog/src/app/components/post/post.component.ts
new file mode 100644
index 000000000000..5b8e7da55eae
--- /dev/null
+++ b/typescript/rest-analog/src/app/components/post/post.component.ts
@@ -0,0 +1,13 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core'
+import { Post } from '../../models/post.model'
+
+@Component({
+ selector: 'app-post',
+ standalone: true,
+ templateUrl: './post.component.html',
+ styleUrl: './post.component.css',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class PostComponent {
+ post = input.required()
+}
diff --git a/typescript/rest-analog/src/app/models/author.model.ts b/typescript/rest-analog/src/app/models/author.model.ts
new file mode 100644
index 000000000000..3ff8005996f5
--- /dev/null
+++ b/typescript/rest-analog/src/app/models/author.model.ts
@@ -0,0 +1,3 @@
+export type Author = {
+ name: string
+}
diff --git a/typescript/rest-analog/src/app/models/post.model.ts b/typescript/rest-analog/src/app/models/post.model.ts
new file mode 100644
index 000000000000..e106192834cd
--- /dev/null
+++ b/typescript/rest-analog/src/app/models/post.model.ts
@@ -0,0 +1,9 @@
+import { Author } from './author.model'
+
+export type Post = {
+ id: number
+ title: string
+ author: Author
+ content: string
+ published: boolean
+}
diff --git a/typescript/rest-analog/src/app/pages/create/index.page.ts b/typescript/rest-analog/src/app/pages/create/index.page.ts
new file mode 100644
index 000000000000..f1bae9440f1d
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/create/index.page.ts
@@ -0,0 +1,136 @@
+import { Component, DestroyRef, inject } from '@angular/core'
+import {
+ AbstractControl,
+ AsyncValidatorFn,
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms'
+import { Router, RouterLink } from '@angular/router'
+import { PostService } from '../../services/post.service'
+import { AuthService } from '../../services/auth.service'
+import { map } from 'rxjs'
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
+
+@Component({
+ selector: 'app-create',
+ standalone: true,
+ template: `
+
+ `,
+ styles: [
+ `
+ .page {
+ background: white;
+ padding: 3rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ input[type='text'],
+ textarea {
+ width: 100%;
+ padding: 0.5rem;
+ margin: 0.5rem 0;
+ border-radius: 0.25rem;
+ border: 0.125rem solid rgba(0, 0, 0, 0.2);
+ }
+
+ input[type='submit'] {
+ background: #ececec;
+ border: 0;
+ padding: 1rem 2rem;
+ }
+
+ .back {
+ margin-left: 1rem;
+ }
+
+ span {
+ color: red;
+ }
+
+ .primary {
+ background: blue !important;
+ color: white;
+ }
+ `,
+ ],
+ imports: [ReactiveFormsModule, RouterLink],
+})
+export default class CreateComponent {
+ #postService = inject(PostService)
+ #authService = inject(AuthService)
+ #router = inject(Router)
+ #destroyRef = inject(DestroyRef)
+
+ form = new FormGroup({
+ title: new FormControl('', [Validators.required]),
+ authorEmail: new FormControl('', {
+ asyncValidators: this.validateEmail(),
+ updateOn: 'blur',
+ }),
+ content: new FormControl(''),
+ })
+
+ createDraft(): void {
+ this.#postService
+ .createDraft(
+ this.form.value.title,
+ this.form.value.authorEmail,
+ this.form.value.content,
+ )
+ .pipe(takeUntilDestroyed(this.#destroyRef))
+ .subscribe({
+ next: () => this.#router.navigate(['/drafts']),
+ error: (err) => console.error(err),
+ })
+ }
+
+ validateEmail(): AsyncValidatorFn {
+ return (control: AbstractControl) => {
+ return this.#authService
+ .findAuthor(control.value)
+ .pipe(map((res) => (Boolean(res) ? null : { authorExists: true })))
+ }
+ }
+}
diff --git a/typescript/rest-analog/src/app/pages/drafts/index.page.ts b/typescript/rest-analog/src/app/pages/drafts/index.page.ts
new file mode 100644
index 000000000000..cd4b5d1b6f08
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/drafts/index.page.ts
@@ -0,0 +1,71 @@
+import { Component, signal } from '@angular/core'
+import { PostComponent } from '../../components/post/post.component'
+import { load } from './index.server'
+import { toSignal } from '@angular/core/rxjs-interop'
+import { injectLoad } from '@analogjs/router'
+import { RouterLink } from '@angular/router'
+import { catchError, EMPTY, tap } from 'rxjs'
+
+@Component({
+ selector: 'app-drafts',
+ standalone: true,
+ template: `
+
+
Drafts
+
+ @if (pending()) {
+
+
+
+ } @else if (error()) {
+ Error while fetching feed 💔
+ } @else {
+
+ @for (post of posts(); track post.id) {
+
+
+
+
+
+ }
+
+ }
+
+
+ `,
+ styles: [
+ `
+ .page {
+ padding-inline: 2rem;
+ }
+
+ .post-list {
+ list-style-type: none;
+ padding: 0;
+ display: grid;
+ gap: 1rem;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ }
+ `,
+ ],
+ imports: [PostComponent, RouterLink],
+})
+export default class DraftsComponent {
+ pending = signal(true)
+ error = signal('')
+
+ posts = toSignal(
+ injectLoad().pipe(
+ tap(() => this.pending.set(false)),
+ catchError((error) => {
+ this.error.set(error.message)
+ return EMPTY
+ }),
+ ),
+ { requireSync: true },
+ )
+}
diff --git a/typescript/rest-analog/src/app/pages/drafts/index.server.ts b/typescript/rest-analog/src/app/pages/drafts/index.server.ts
new file mode 100644
index 000000000000..7f494a5f87d5
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/drafts/index.server.ts
@@ -0,0 +1,6 @@
+import { PageServerLoad } from '@analogjs/router'
+import { Post } from '../../models/post.model'
+
+export const load = async ({ fetch }: PageServerLoad): Promise => {
+ return await fetch('/draft-list')
+}
diff --git a/typescript/rest-analog/src/app/pages/index.page.ts b/typescript/rest-analog/src/app/pages/index.page.ts
new file mode 100644
index 000000000000..9ad48306f027
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/index.page.ts
@@ -0,0 +1,66 @@
+import { Component, signal } from '@angular/core'
+import { PostComponent } from '../components/post/post.component'
+import { load } from './index.server'
+import { toSignal } from '@angular/core/rxjs-interop'
+import { injectLoad } from '@analogjs/router'
+import { catchError, EMPTY, tap } from 'rxjs'
+
+@Component({
+ selector: 'app-home',
+ standalone: true,
+ template: `
+
+
My Blog
+
+ @if (pending()) {
+
+
+
+ } @else if (error()) {
+ Error while fetching feed 💔
+ } @else {
+ @for (post of posts(); track post.id) {
+
+ }
+ }
+
+
+ `,
+ styles: [
+ `
+ .page {
+ padding-inline: 3rem;
+ }
+
+ .post {
+ background: white;
+ transition: box-shadow 0.1s ease-in;
+ }
+
+ .post:hover {
+ box-shadow: 1px 1px 3px #aaa;
+ }
+
+ .post,
+ .post {
+ margin-top: 2rem;
+ }
+ `,
+ ],
+ imports: [PostComponent],
+})
+export default class HomeComponent {
+ pending = signal(true)
+ error = signal('')
+
+ posts = toSignal(
+ injectLoad().pipe(
+ tap(() => this.pending.set(false)),
+ catchError((error) => {
+ this.error.set(error.message)
+ return EMPTY
+ }),
+ ),
+ { requireSync: true },
+ )
+}
diff --git a/typescript/rest-analog/src/app/pages/index.server.ts b/typescript/rest-analog/src/app/pages/index.server.ts
new file mode 100644
index 000000000000..2cd07e341674
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/index.server.ts
@@ -0,0 +1,6 @@
+import { PageServerLoad } from '@analogjs/router'
+import { Post } from '../models/post.model'
+
+export const load = async ({ fetch }: PageServerLoad): Promise => {
+ return await fetch('/api/feed')
+}
diff --git a/typescript/rest-analog/src/app/pages/post/[id].page.ts b/typescript/rest-analog/src/app/pages/post/[id].page.ts
new file mode 100644
index 000000000000..dd92173a88cb
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/post/[id].page.ts
@@ -0,0 +1,108 @@
+import { Component, DestroyRef, inject, signal } from '@angular/core'
+import { load } from './[id].server'
+import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'
+import { injectLoad } from '@analogjs/router'
+import { catchError, EMPTY, tap } from 'rxjs'
+import { PostService } from '../../services/post.service'
+import { Router } from '@angular/router'
+
+@Component({
+ selector: 'app-post-item',
+ standalone: true,
+ template: `
+
+ @if (pending()) {
+
+
+
+ } @else if (error()) {
+
Error while fetching feed 💔
+ } @else {
+
+
+ {{ article().title }} ({{
+ article().published ? 'Published' : 'Draft'
+ }})
+
+ By {{ article().author.name }}
+
+
+ @if (!article().published) {
+ Publish
+ }
+ Delete
+
+
+ }
+
+ `,
+ styles: [
+ `
+ .page {
+ background: white;
+ padding: 2rem;
+ }
+
+ .actions {
+ margin-top: 2rem;
+ }
+
+ button {
+ margin: 0.5rem;
+ background: #ececec;
+ border: 1px black solid;
+ border-radius: 0.125rem;
+ padding: 1rem 2rem;
+ }
+
+ button button {
+ margin-left: 1rem;
+ }
+
+ .btn-wrapper {
+ display: flex;
+ justify-content: center;
+ width: fit-content;
+ margin-top: 1rem;
+ }
+ `,
+ ],
+ imports: [],
+})
+export default class PostItemComponent {
+ #postService = inject(PostService)
+ #router = inject(Router)
+ #destroyRef = inject(DestroyRef)
+
+ pending = signal(true)
+ error = signal('')
+
+ article = toSignal(
+ injectLoad().pipe(
+ tap(() => this.pending.set(false)),
+ catchError((error) => {
+ this.error.set(error.message)
+ return EMPTY
+ }),
+ ),
+ { requireSync: true },
+ )
+
+ destroy(): void {
+ this.#postService
+ .destroy(this.article().id)
+ .pipe(takeUntilDestroyed(this.#destroyRef))
+ .subscribe(() => {
+ void this.#router.navigate(['/'])
+ })
+ }
+
+ publish(): void {
+ this.#postService
+ .publish(this.article().id)
+ .pipe(takeUntilDestroyed(this.#destroyRef))
+ .subscribe(() => {
+ void this.#router.navigate(['/'])
+ })
+ }
+}
diff --git a/typescript/rest-analog/src/app/pages/post/[id].server.ts b/typescript/rest-analog/src/app/pages/post/[id].server.ts
new file mode 100644
index 000000000000..035672ab82bb
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/post/[id].server.ts
@@ -0,0 +1,9 @@
+import { PageServerLoad } from '@analogjs/router'
+import { Post } from '../../models/post.model'
+
+export const load = async ({
+ fetch,
+ params,
+}: PageServerLoad): Promise => {
+ return await fetch(`/api/post/${params?.['id']}`)
+}
diff --git a/typescript/rest-analog/src/app/pages/signup/index.page.ts b/typescript/rest-analog/src/app/pages/signup/index.page.ts
new file mode 100644
index 000000000000..33386cbc35cf
--- /dev/null
+++ b/typescript/rest-analog/src/app/pages/signup/index.page.ts
@@ -0,0 +1,95 @@
+import { Component, DestroyRef, inject } from '@angular/core'
+import { Router, RouterLink } from '@angular/router'
+import {
+ FormControl,
+ FormGroup,
+ ReactiveFormsModule,
+ Validators,
+} from '@angular/forms'
+import { AuthService } from '../../services/auth.service'
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
+
+type AuthForm = {
+ name: FormControl
+ email: FormControl
+}
+
+@Component({
+ selector: 'app-signup',
+ standalone: true,
+ template: `
+
+ `,
+ styles: [
+ `
+ .page {
+ background: white;
+ padding: 3rem;
+ display: flex;
+ justify-content: center;
+ }
+
+ input[type='text'] {
+ width: 100%;
+ padding: 0.5rem;
+ margin: 0.5rem 0;
+ border-radius: 0.25rem;
+ border: 0.125rem solid rgba(0, 0, 0, 0.2);
+ }
+
+ input[type='submit'] {
+ background: #ececec;
+ border: 0;
+ padding: 1rem 2rem;
+ }
+
+ .back {
+ margin-left: 1rem;
+ }
+ `,
+ ],
+ imports: [RouterLink, ReactiveFormsModule],
+})
+export default class SignupComponent {
+ #authService = inject(AuthService)
+ #router = inject(Router)
+ #destroyRef = inject(DestroyRef)
+
+ form = new FormGroup({
+ name: new FormControl('', {
+ validators: [Validators.required],
+ nonNullable: true,
+ }),
+ email: new FormControl('', {
+ validators: [Validators.required],
+ nonNullable: true,
+ }),
+ })
+
+ signup(): void {
+ this.#authService
+ .signup(this.form.value.name, this.form.value.email)
+ .pipe(takeUntilDestroyed(this.#destroyRef))
+ .subscribe({
+ next: () => this.#router.navigate(['/']),
+ error: (error) => console.error(error),
+ })
+ }
+}
diff --git a/typescript/rest-analog/src/app/services/auth.service.ts b/typescript/rest-analog/src/app/services/auth.service.ts
new file mode 100644
index 000000000000..5eea3865d49e
--- /dev/null
+++ b/typescript/rest-analog/src/app/services/auth.service.ts
@@ -0,0 +1,17 @@
+import { inject, Injectable } from '@angular/core'
+import { HttpClient } from '@angular/common/http'
+
+@Injectable({
+ providedIn: 'root',
+})
+export class AuthService {
+ #http = inject(HttpClient)
+
+ signup(name: string, email: string) {
+ return this.#http.post('/api/user', { name, email })
+ }
+
+ findAuthor(email: string) {
+ return this.#http.post(`/api/author`, { email })
+ }
+}
diff --git a/typescript/rest-analog/src/app/services/post.service.ts b/typescript/rest-analog/src/app/services/post.service.ts
new file mode 100644
index 000000000000..499b7c81dd22
--- /dev/null
+++ b/typescript/rest-analog/src/app/services/post.service.ts
@@ -0,0 +1,21 @@
+import { inject, Injectable } from '@angular/core'
+import { HttpClient } from '@angular/common/http'
+
+@Injectable({
+ providedIn: 'root',
+})
+export class PostService {
+ #http = inject(HttpClient)
+
+ createDraft(title: string, authorEmail: string, content: string) {
+ return this.#http.post('/api/post', { title, authorEmail, content })
+ }
+
+ publish(id: number) {
+ return this.#http.put(`/api/publish/${id}`, {})
+ }
+
+ destroy(id: number) {
+ return this.#http.delete(`/api/post/${id}`)
+ }
+}
diff --git a/typescript/rest-analog/src/main.server.ts b/typescript/rest-analog/src/main.server.ts
new file mode 100644
index 000000000000..66f02a3f5d22
--- /dev/null
+++ b/typescript/rest-analog/src/main.server.ts
@@ -0,0 +1,32 @@
+import 'zone.js/node'
+import '@angular/platform-server/init'
+import { enableProdMode } from '@angular/core'
+import { bootstrapApplication } from '@angular/platform-browser'
+import { renderApplication } from '@angular/platform-server'
+import { provideServerContext } from '@analogjs/router/server'
+import { ServerContext } from '@analogjs/router/tokens'
+
+import { config } from './app/app.config.server'
+import App from './app/app.analog'
+
+if (import.meta.env.PROD) {
+ enableProdMode()
+}
+
+export function bootstrap() {
+ return bootstrapApplication(App, config)
+}
+
+export default async function render(
+ url: string,
+ document: string,
+ serverContext: ServerContext,
+) {
+ const html = await renderApplication(bootstrap, {
+ document,
+ url,
+ platformProviders: [provideServerContext(serverContext)],
+ })
+
+ return html
+}
diff --git a/typescript/rest-analog/src/main.ts b/typescript/rest-analog/src/main.ts
new file mode 100644
index 000000000000..6aed47879242
--- /dev/null
+++ b/typescript/rest-analog/src/main.ts
@@ -0,0 +1,7 @@
+import 'zone.js'
+import { bootstrapApplication } from '@angular/platform-browser'
+
+import App from './app/app.analog';
+import { appConfig } from './app/app.config'
+
+bootstrapApplication(App, appConfig)
diff --git a/typescript/rest-analog/src/server/routes/author.ts b/typescript/rest-analog/src/server/routes/author.ts
new file mode 100644
index 000000000000..c4660609db81
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/author.ts
@@ -0,0 +1,17 @@
+import { prisma } from '../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const { email } = await readBody(event)
+
+ try {
+ const user = await prisma.user.findFirst({
+ where: {
+ email: email,
+ },
+ })
+
+ return user
+ } catch (error) {
+ console.error(error)
+ }
+})
diff --git a/typescript/rest-analog/src/server/routes/draft-list.get.ts b/typescript/rest-analog/src/server/routes/draft-list.get.ts
new file mode 100644
index 000000000000..9019b46a7bcd
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/draft-list.get.ts
@@ -0,0 +1,18 @@
+import { prisma } from '../../../prisma/db'
+
+export default defineEventHandler(async () => {
+ const posts = await prisma.post
+ .findMany({
+ where: {
+ published: false,
+ },
+ include: {
+ author: true,
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return posts
+})
diff --git a/typescript/rest-analog/src/server/routes/feed.get.ts b/typescript/rest-analog/src/server/routes/feed.get.ts
new file mode 100644
index 000000000000..4df02d9be041
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/feed.get.ts
@@ -0,0 +1,18 @@
+import { prisma } from '../../../prisma/db'
+
+export default defineEventHandler(async () => {
+ const feed = await prisma.post
+ .findMany({
+ where: {
+ published: true,
+ },
+ include: {
+ author: true,
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return feed
+})
diff --git a/typescript/rest-analog/src/server/routes/filterPosts.get.ts b/typescript/rest-analog/src/server/routes/filterPosts.get.ts
new file mode 100644
index 000000000000..048fa0e7a58f
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/filterPosts.get.ts
@@ -0,0 +1,28 @@
+import { prisma } from '../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const { searchString } = getQuery(event)
+
+ const draftPosts = await prisma.post
+ .findMany({
+ where: {
+ OR: [
+ {
+ title: {
+ contains: searchString,
+ },
+ },
+ {
+ content: {
+ contains: searchString,
+ },
+ },
+ ],
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return draftPosts
+})
diff --git a/typescript/rest-analog/src/server/routes/post/[id].delete.ts b/typescript/rest-analog/src/server/routes/post/[id].delete.ts
new file mode 100644
index 000000000000..3b7e3677a50d
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/post/[id].delete.ts
@@ -0,0 +1,18 @@
+import { prisma } from '../../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const id = event.context.params.id
+
+ const deletePost = await prisma.post
+ .delete({
+ where: {
+ //@ts-ignore
+ id: parseInt(id),
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return deletePost
+})
diff --git a/typescript/rest-analog/src/server/routes/post/[id].get.ts b/typescript/rest-analog/src/server/routes/post/[id].get.ts
new file mode 100644
index 000000000000..2897cfda62e9
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/post/[id].get.ts
@@ -0,0 +1,25 @@
+import { prisma } from '../../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const {
+ context: {
+ params: { id },
+ },
+ } = event
+
+ const getPost = await prisma.post
+ .findUnique({
+ where: {
+ //@ts-ignore
+ id: parseInt(id),
+ },
+ include: {
+ author: true,
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return getPost
+})
diff --git a/typescript/rest-analog/src/server/routes/post/index.ts b/typescript/rest-analog/src/server/routes/post/index.ts
new file mode 100644
index 000000000000..d6d1c652e336
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/post/index.ts
@@ -0,0 +1,24 @@
+import { prisma } from '../../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const { title, content, authorEmail } = await readBody(event)
+
+ const createPost = await prisma.post
+ .create({
+ data: {
+ title,
+ content,
+ published: false,
+ author: {
+ connect: {
+ email: authorEmail,
+ },
+ },
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return createPost
+})
diff --git a/typescript/rest-analog/src/server/routes/publish/[id].put.ts b/typescript/rest-analog/src/server/routes/publish/[id].put.ts
new file mode 100644
index 000000000000..3c8581df0fd9
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/publish/[id].put.ts
@@ -0,0 +1,20 @@
+import { prisma } from '../../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const id = event.context.params.id
+
+ const updatePost = await prisma.post
+ .update({
+ where: {
+ id: parseInt(id),
+ },
+ data: {
+ published: true,
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return updatePost
+})
diff --git a/typescript/rest-analog/src/server/routes/user.ts b/typescript/rest-analog/src/server/routes/user.ts
new file mode 100644
index 000000000000..fb9def089fb9
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/user.ts
@@ -0,0 +1,18 @@
+import { prisma } from '../../../prisma/db'
+
+export default defineEventHandler(async (event) => {
+ const { name, email } = await readBody(event)
+
+ const createUser = await prisma.user
+ .create({
+ data: {
+ name,
+ email,
+ },
+ })
+ .catch((error) => {
+ console.error(error)
+ })
+
+ return createUser
+})
diff --git a/typescript/rest-analog/src/server/routes/v1/hello.ts b/typescript/rest-analog/src/server/routes/v1/hello.ts
new file mode 100644
index 000000000000..594c5d716df1
--- /dev/null
+++ b/typescript/rest-analog/src/server/routes/v1/hello.ts
@@ -0,0 +1,3 @@
+import { defineEventHandler } from 'h3';
+
+export default defineEventHandler(() => ({ message: 'Hello World' }));
diff --git a/typescript/rest-analog/src/styles.css b/typescript/rest-analog/src/styles.css
new file mode 100644
index 000000000000..710e162c95c5
--- /dev/null
+++ b/typescript/rest-analog/src/styles.css
@@ -0,0 +1,31 @@
+html {
+ box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+ font-size: 16px;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
+ Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
+ background: rgba(0, 0, 0, 0.05);
+}
+
+input,
+textarea {
+ font-size: 16px;
+}
+
+button {
+ cursor: pointer;
+}
+
+.layout {
+ padding: 0 2rem;
+}
diff --git a/typescript/rest-analog/src/test-setup.ts b/typescript/rest-analog/src/test-setup.ts
new file mode 100644
index 000000000000..cb5fd340f3d8
--- /dev/null
+++ b/typescript/rest-analog/src/test-setup.ts
@@ -0,0 +1,12 @@
+import '@analogjs/vite-plugin-angular/setup-vitest'
+
+import {
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting,
+} from '@angular/platform-browser-dynamic/testing'
+import { getTestBed } from '@angular/core/testing'
+
+getTestBed().initTestEnvironment(
+ BrowserDynamicTestingModule,
+ platformBrowserDynamicTesting(),
+)
diff --git a/typescript/rest-analog/src/vite-env.d.ts b/typescript/rest-analog/src/vite-env.d.ts
new file mode 100644
index 000000000000..11f02fe2a006
--- /dev/null
+++ b/typescript/rest-analog/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/typescript/rest-analog/tsconfig.app.json b/typescript/rest-analog/tsconfig.app.json
new file mode 100644
index 000000000000..03a3f71a8661
--- /dev/null
+++ b/typescript/rest-analog/tsconfig.app.json
@@ -0,0 +1,10 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/app",
+ "types": []
+ },
+ "files": ["src/main.ts", "src/main.server.ts"],
+ "include": ["src/**/*.d.ts", "src/app/pages/**/*.page.ts"]
+}
diff --git a/typescript/rest-analog/tsconfig.json b/typescript/rest-analog/tsconfig.json
new file mode 100644
index 000000000000..94e11863a100
--- /dev/null
+++ b/typescript/rest-analog/tsconfig.json
@@ -0,0 +1,31 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "compileOnSave": false,
+ "compilerOptions": {
+ "baseUrl": "./",
+ "outDir": "./dist/out-tsc",
+ "forceConsistentCasingInFileNames": true,
+ "strict": true,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "sourceMap": true,
+ "declaration": false,
+ "downlevelIteration": true,
+ "experimentalDecorators": true,
+ "moduleResolution": "node",
+ "importHelpers": true,
+ "target": "ES2022",
+ "module": "ES2022",
+ "lib": ["ES2022", "dom"],
+ "useDefineForClassFields": false,
+ "skipLibCheck": true
+ },
+ "angularCompilerOptions": {
+ "enableI18nLegacyMessageIdFormat": false,
+ "strictInjectionParameters": true,
+ "strictInputAccessModifiers": true,
+ "strictTemplates": true
+ }
+}
diff --git a/typescript/rest-analog/tsconfig.spec.json b/typescript/rest-analog/tsconfig.spec.json
new file mode 100644
index 000000000000..06eb7cae6180
--- /dev/null
+++ b/typescript/rest-analog/tsconfig.spec.json
@@ -0,0 +1,11 @@
+/* To learn more about this file see: https://angular.io/config/tsconfig. */
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./out-tsc/spec",
+ "target": "es2016",
+ "types": ["node", "vitest/globals"]
+ },
+ "files": ["src/test-setup.ts"],
+ "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
+}
diff --git a/typescript/rest-analog/vite.config.ts b/typescript/rest-analog/vite.config.ts
new file mode 100644
index 000000000000..9fab67107127
--- /dev/null
+++ b/typescript/rest-analog/vite.config.ts
@@ -0,0 +1,34 @@
+///
+
+import { defineConfig } from 'vite'
+import analog from '@analogjs/platform'
+
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => ({
+ build: {
+ target: ['es2020'],
+ },
+ resolve: {
+ mainFields: ['module'],
+ },
+ plugins: [
+ analog({
+ vite: {
+ // Required to use the Analog SFC format
+ experimental: {
+ supportAnalogFormat: true,
+ },
+ },
+ }),
+ ],
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['src/test-setup.ts'],
+ include: ['**/*.spec.ts'],
+ reporters: ['default'],
+ },
+ define: {
+ 'import.meta.vitest': mode !== 'production',
+ },
+}))