├── .gitignore
├── README.md
├── angular.json
├── package-lock.json
├── package.json
├── server
└── main.ts
├── src
├── @types
│ └── global.d.ts
├── app
│ ├── about
│ │ ├── about.component.css
│ │ ├── about.component.html
│ │ ├── about.component.spec.ts
│ │ └── about.component.ts
│ ├── app.component.css
│ ├── app.component.html
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.config.browser.ts
│ ├── app.config.server.ts
│ ├── app.config.ts
│ ├── app.routes.ts
│ ├── home
│ │ ├── ServerService
│ │ │ ├── TransferState.ts
│ │ │ ├── example.service.browser.ts
│ │ │ ├── example.service.server.ts
│ │ │ └── index.ts
│ │ ├── home.component.css
│ │ ├── home.component.html
│ │ ├── home.component.spec.ts
│ │ └── home.component.ts
│ └── todos
│ │ ├── todos.component.css
│ │ ├── todos.component.html
│ │ ├── todos.component.spec.ts
│ │ ├── todos.component.ts
│ │ └── todos.service.ts
├── assets
│ └── .gitkeep
├── bootstrap.browser.ts
├── bootstrap.server.ts
├── favicon.ico
├── index.html
├── main.ts
└── styles.css
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.server.json
├── tsconfig.spec.json
└── webpack.config.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .DS_Store
4 | .angular
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Angular Server Services 🍑
2 |
3 | This project presents an example of Angular Server Services implementation, utilizing proxying for client services to make RPC (Remote Procedure Calls) to the server service. The goal of this example is to demonstrate how easily these services can be auto-generated, potentially reducing half of your existing codebase.
4 |
5 | Whats used in the repo:
6 | * Angular 16
7 | * Standalone Components
8 | * Universal
9 | * Hydration
10 | * NgExpressEngine
11 | * Custom Webpack
12 |
13 | > Please note, we are currently using injection-js to bypass the Angular injector and micotask to force zone.js to wait on the server-side. This workarounds are only needed to workaround how Angular bootstraps and manages the apps on the server. I am also creating my own TransferState service just for this demo.
14 |
15 | ## Why
16 |
17 | The goal is to replicate GraphQL or RSC by moving all domain logic that lives in these services to the server. We want to do this so we can utilize caching on the server and remove all client code for these services (usually half your codebase)
18 |
19 | Angular can easily support this pattern in Angular Universal with little effort.
20 |
21 | ## Start
22 |
23 | ```bash
24 | $ npm install
25 | $ npm run dev:ssr
26 | ```
27 | go to [localhost ](http://localhost:4200/)
28 |
29 |
30 |
31 |
32 | Initial load uses transfer state. When you navigate to another page the back to Home we will make an RPC to get the updated state
33 |
34 | * [/app/home/ServerService/example.service.server.ts](https://github.com/PatrickJS/angular-server-services/blob/main/src/app/home/ServerService/example.service.server.ts): ServerService example
35 | * [/server/main.ts](https://github.com/PatrickJS/angular-server-services/blob/e5deec3011d17c1f7301b848eb3f88d268ea8454/server/main.ts#L36...L45): server RPC endpoint
36 | * [/app/app.config.browser](https://github.com/PatrickJS/angular-server-services/blob/main/src/app/app.config.browser.ts#L10...L38): client RPC requests
37 |
38 | If we had Angular support then the api would look like this (a lot less code)
39 | * [branch for ideal api](https://github.com/PatrickJS/angular-server-services/tree/ideal-api)
40 | * [ExampleService](https://github.com/PatrickJS/angular-server-services/blob/ideal-api/src/%40server/Example.service.ts)
41 | * [HomeComponent](https://github.com/PatrickJS/angular-server-services/blob/ideal-api/src/app/home/home.component.ts#L4)
42 |
43 | Production ready version
44 | * WIP https://github.com/PatrickJS/angular-server-services/pull/2
45 | * Preview
46 |
47 | https://github.com/PatrickJS/angular-server-services/assets/1016365/8b00d775-42c4-4d29-b79a-815906d35d04
48 |
49 |
50 | # TODO: production ready version
51 | - [x] use webpack to auto-generate ServerServices
52 | - [x] create @server folder in src that will be all server services and components
53 | - [x] use angular TransferState
54 | - [x] batch client requests
55 | - [x] batch server requests
56 | - [ ] server commponents
57 | - [ ] hook into router to batch requests for server components
58 | - [ ] mixed server in client components and vice versa
59 | - [ ] server and client caching
60 | - [ ] UI over http
61 |
62 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-server-services": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "standalone": true
11 | },
12 | "@schematics/angular:directive": {
13 | "standalone": true
14 | },
15 | "@schematics/angular:pipe": {
16 | "standalone": true
17 | }
18 | },
19 | "root": "",
20 | "sourceRoot": "src",
21 | "prefix": "app",
22 | "architect": {
23 | "build": {
24 | "builder": "@angular-builders/custom-webpack:browser",
25 | "options": {
26 | "optimization": false,
27 | "customWebpackConfig": {
28 | "path": "./webpack.config.ts"
29 | },
30 | "outputPath": "dist/angular-server-services/browser",
31 | "index": "src/index.html",
32 | "main": "src/main.ts",
33 | "polyfills": [
34 | "zone.js"
35 | ],
36 | "tsConfig": "tsconfig.app.json",
37 | "assets": [
38 | "src/favicon.ico",
39 | "src/assets"
40 | ],
41 | "styles": [
42 | "src/styles.css"
43 | ],
44 | "scripts": []
45 | },
46 | "configurations": {
47 | "production": {
48 | "budgets": [
49 | {
50 | "type": "initial",
51 | "maximumWarning": "500kb",
52 | "maximumError": "5mb"
53 | },
54 | {
55 | "type": "anyComponentStyle",
56 | "maximumWarning": "2kb",
57 | "maximumError": "4kb"
58 | }
59 | ],
60 | "outputHashing": "all"
61 | },
62 | "development": {
63 | "buildOptimizer": false,
64 | "optimization": false,
65 | "vendorChunk": true,
66 | "extractLicenses": false,
67 | "sourceMap": true,
68 | "namedChunks": true
69 | }
70 | },
71 | "defaultConfiguration": "production"
72 | },
73 | "serve": {
74 | "builder": "@angular-builders/custom-webpack:dev-server",
75 | "options": {
76 | "customWebpackConfig": {
77 | "path": "./webpack.config.ts"
78 | }
79 | },
80 | "configurations": {
81 | "production": {
82 | "browserTarget": "angular-server-services:build:production"
83 | },
84 | "development": {
85 | "browserTarget": "angular-server-services:build:development"
86 | }
87 | },
88 | "defaultConfiguration": "development"
89 | },
90 | "extract-i18n": {
91 | "builder": "@angular-devkit/build-angular:extract-i18n",
92 | "options": {
93 | "browserTarget": "angular-server-services:build"
94 | }
95 | },
96 | "test": {
97 | "builder": "@angular-builders/custom-webpack:karma",
98 | "options": {
99 | "polyfills": [
100 | "zone.js",
101 | "zone.js/testing"
102 | ],
103 | "tsConfig": "tsconfig.spec.json",
104 | "assets": [
105 | "src/favicon.ico",
106 | "src/assets"
107 | ],
108 | "styles": [
109 | "src/styles.css"
110 | ],
111 | "scripts": []
112 | }
113 | },
114 | "server": {
115 | "builder": "@angular-builders/custom-webpack:server",
116 | "options": {
117 | "customWebpackConfig": {
118 | "path": "./webpack.config.ts"
119 | },
120 | "optimization": false,
121 | "outputPath": "dist/angular-server-services/server",
122 | "main": "server/main.ts",
123 | "tsConfig": "tsconfig.server.json"
124 | },
125 | "configurations": {
126 | "production": {
127 | "outputHashing": "media"
128 | },
129 | "development": {
130 | "optimization": false,
131 | "sourceMap": true,
132 | "extractLicenses": false,
133 | "vendorChunk": true
134 | }
135 | },
136 | "defaultConfiguration": "production"
137 | },
138 | "serve-ssr": {
139 | "builder": "@nguniversal/builders:ssr-dev-server",
140 | "configurations": {
141 | "development": {
142 | "browserTarget": "angular-server-services:build:development",
143 | "serverTarget": "angular-server-services:server:development"
144 | },
145 | "production": {
146 | "browserTarget": "angular-server-services:build:production",
147 | "serverTarget": "angular-server-services:server:production"
148 | }
149 | },
150 | "defaultConfiguration": "development"
151 | },
152 | "prerender": {
153 | "builder": "@nguniversal/builders:prerender",
154 | "options": {
155 | "routes": [
156 | "/",
157 | "/about",
158 | "/todos"
159 | ]
160 | },
161 | "configurations": {
162 | "production": {
163 | "browserTarget": "angular-server-services:build:production",
164 | "serverTarget": "angular-server-services:server:production"
165 | },
166 | "development": {
167 | "browserTarget": "angular-server-services:build:development",
168 | "serverTarget": "angular-server-services:server:development"
169 | }
170 | },
171 | "defaultConfiguration": "production"
172 | }
173 | }
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-server-services",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test",
10 | "dev:ssr": "ng run angular-server-services:serve-ssr",
11 | "serve:ssr": "node dist/angular-server-services/server/main.js",
12 | "build:ssr": "ng build && ng run angular-server-services:server",
13 | "prerender": "ng run angular-server-services:prerender"
14 | },
15 | "private": true,
16 | "dependencies": {
17 | "@angular-builders/custom-webpack": "16.0.0-beta.1",
18 | "@angular/animations": "~16.0.3",
19 | "@angular/common": "~16.0.3",
20 | "@angular/compiler": "~16.0.3",
21 | "@angular/core": "~16.0.3",
22 | "@angular/forms": "~16.0.3",
23 | "@angular/platform-browser": "~16.0.3",
24 | "@angular/platform-browser-dynamic": "~16.0.3",
25 | "@angular/platform-server": "~16.0.3",
26 | "@angular/router": "~16.0.3",
27 | "@nguniversal/express-engine": "^16.0.0-next.0",
28 | "body-parser": "1.20.2",
29 | "cross-fetch": "3.1.6",
30 | "express": "^4.15.2",
31 | "injection-js": "2.4.0",
32 | "is-browser": "2.1.0",
33 | "reflect-metadata": "0.1.13",
34 | "rxjs": "~7.8.0",
35 | "tslib": "^2.3.0",
36 | "zone.js": "~0.13.0"
37 | },
38 | "devDependencies": {
39 | "@angular-devkit/build-angular": "^16.0.3",
40 | "@angular/cli": "~16.0.3",
41 | "@angular/compiler-cli": "~16.0.3",
42 | "@nguniversal/builders": "^16.0.0-next.0",
43 | "@types/express": "^4.17.0",
44 | "@types/jasmine": "~4.3.0",
45 | "@types/node": "^14.15.0",
46 | "jasmine-core": "~4.6.0",
47 | "karma": "~6.4.0",
48 | "karma-chrome-launcher": "~3.1.0",
49 | "karma-coverage": "~2.2.0",
50 | "karma-jasmine": "~5.1.0",
51 | "karma-jasmine-html-reporter": "~2.0.0",
52 | "typescript": "~5.0.2"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/server/main.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata'; // injection-js
2 | import 'cross-fetch/polyfill';
3 | import 'zone.js/node';
4 | import 'zone.js/dist/zone-patch-fetch';
5 |
6 | import { APP_BASE_HREF } from '@angular/common';
7 | import { ngExpressEngine } from '@nguniversal/express-engine';
8 | import * as express from 'express';
9 | import * as bodyParser from 'body-parser';
10 | import { existsSync } from 'node:fs';
11 | import { join } from 'node:path';
12 | import bootstrap, {injector, transferState} from '../src/bootstrap.server';
13 |
14 | // The Express app is exported so that it can be used by serverless Functions.
15 | export function app(): express.Express {
16 | const server = express();
17 | const distFolder = join(process.cwd(), 'dist/angular-server-services/browser');
18 | const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';
19 |
20 | // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
21 | server.engine('html', ngExpressEngine({
22 | bootstrap
23 | }));
24 |
25 | server.set('view engine', 'html');
26 | server.set('views', distFolder);
27 |
28 | server.use(bodyParser.json());
29 |
30 |
31 | // Example Express Rest API endpoints
32 | server.get('/api/**', (req, res) => {
33 | });
34 |
35 | // TODO: auto generate this in ngExpressEngine to get injector
36 | server.post('/angular-server-services/:Service/:Method', (req, res) => {
37 | const service = injector.get(req.params.Service);
38 | console.log('angular-server-service request: service', req.params.Service)
39 | const method = service[req.params.Method];
40 | console.log('angular-server-service request: method', req.params.Method)
41 | console.log('angular-server-service request: body', req.body)
42 | method.apply(service, req.body).then((result: any) => {
43 | res.json(result);
44 | });
45 | });
46 |
47 | // Serve static files from /browser
48 | server.get('*.*', express.static(distFolder, {
49 | maxAge: '0'
50 | }));
51 |
52 | // All regular routes use the Universal engine
53 | server.get('*', (req, res) => {
54 | // TODO: better transfer state
55 | const state = {};
56 | transferState._state = state;
57 | res.render(indexHtml, {
58 | req,
59 | providers: [
60 | { provide: APP_BASE_HREF, useValue: req.baseUrl },
61 | ],
62 | }, (err, html) =>{
63 | if (err) {
64 | console.error(err);
65 | res.send(err);
66 | }
67 | console.log('SSR done');
68 | // TODO: better transfer state
69 | // TODO: auto generate this
70 | res.send(html.replace(//, ``));
71 | });
72 | });
73 |
74 | return server;
75 | }
76 |
77 | function run(): void {
78 | const port = process.env['PORT'] || 4000;
79 |
80 | // Start up the Node server
81 | const server = app();
82 | server.listen(port, () => {
83 | console.log(`Node Express server listening on http://localhost:${port}`);
84 | });
85 | }
86 |
87 | // Webpack will replace 'require' with '__webpack_require__'
88 | // '__non_webpack_require__' is a proxy to Node 'require'
89 | // The below code is to ensure that the server is run only when not requiring the bundle.
90 | declare const __non_webpack_require__: NodeRequire;
91 | const mainModule = __non_webpack_require__.main;
92 | const moduleFilename = mainModule && mainModule.filename || '';
93 | if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
94 | run();
95 | }
96 |
97 | export * from '../src/bootstrap.server';
98 |
99 | // fixes prerendering
100 | export default bootstrap;
--------------------------------------------------------------------------------
/src/@types/global.d.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/zone.d.ts';
2 |
3 | // ignore this typescript thing
4 | export {};
5 |
6 | // declare globals
7 | declare global {
8 | const APP_VERSION: string;
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/about/about.component.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/johnfrench3/angular-server-repos/7bf87941f0f07cbc15772e7a97fc3566d351d002/src/app/about/about.component.css
--------------------------------------------------------------------------------
/src/app/about/about.component.html:
--------------------------------------------------------------------------------
1 |
about works!
2 | 3 | -------------------------------------------------------------------------------- /src/app/about/about.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { AboutComponent } from './about.component'; 4 | 5 | describe('AboutComponent', () => { 6 | let component: AboutComponent; 7 | let fixture: ComponentFixturehome works!
2 | APP_VERSION: {{ APP_VERSION }} 3 | 4 |{{ example | async | json }}-------------------------------------------------------------------------------- /src/app/home/home.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { HomeComponent } from './home.component'; 4 | 5 | describe('HomeComponent', () => { 6 | let component: HomeComponent; 7 | let fixture: ComponentFixture