Skip to content

Commit ba968de

Browse files
docs: Add documentation for client-side endpoint inheritance (serverpod#333)
1 parent df05043 commit ba968de

1 file changed

Lines changed: 170 additions & 7 deletions

File tree

docs/06-concepts/01-working-with-endpoints.md

Lines changed: 170 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ apiServer:
4444
publicHost: localhost # Change this line
4545
publicPort: 8080
4646
publicScheme: http
47-
...
4847
```
4948
5049
:::info
@@ -77,7 +76,7 @@ The return type must be a typed Future. Supported return types are the same as f
7776

7877
### Ignore an entire `Endpoint` class
7978

80-
If you want the code generator to ignore an endpoint definition, you can annotate either the entire class or individual methods with `@doNotGenerate`. This can be useful if you want to keep the definition in your codebase without generating server or client bindings for it.
79+
If you want the code generator to ignore an endpoint definition, you can annotate either the entire class or individual methods with `@doNotGenerate`. This can be useful if you want to keep the definition in your codebase without generating server or client bindings for it.
8180

8281
```dart
8382
import 'package:serverpod/serverpod.dart';
@@ -115,7 +114,7 @@ In this case the `ExampleEndpoint` will only expose the `hello` method, whereas
115114

116115
## Endpoint method inheritance
117116

118-
Endpoints can be based on other endpoints using inheritance, like `class ChildEndpoint extends ParentEndpoint`. If the parent endpoint was marked as `abstract` or `@doNotGenerate`, no client code is generated for it, but a client will be generated for your subclass – as long as it does not opt out again.
117+
Endpoints can be based on other endpoints using inheritance, like `class ChildEndpoint extends ParentEndpoint`. If the parent endpoint was marked as `abstract` or `@doNotGenerate`, no client code is generated for it, but a client will be generated for your subclass – as long as it does not opt out again.
119118
Inheritance gives you the possibility to modify the behavior of `Endpoint` classes defined in other Serverpod modules.
120119

121120
Currently, there are the following possibilities to extend another `Endpoint` class:
@@ -159,8 +158,7 @@ abstract class CalculatorEndpoint extends Endpoint {
159158
class MyCalculatorEndpoint extends CalculatorEndpoint {}
160159
```
161160

162-
The generated client code will only be able to access `MyCalculatorEndpoint`, as the abstract `CalculatorEndpoint` is not exposed on the server.
163-
`MyCalculatorEndpoint` exposes the `add` method it inherited from `CalculatorEndpoint`.
161+
Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. However, an abstract client class will be generated, which will be extended by the class generated from `MyCalculatorEndpoint`. The concrete client exposes the `add` method it inherited from `CalculatorEndpoint`. See [Client-side endpoint inheritance](#client-side-endpoint-inheritance) for more details on how abstract endpoints are represented on the client.
164162

165163
#### Extending an `abstract` `Endpoint` class
166164

@@ -195,7 +193,7 @@ class CalculatorEndpoint extends Endpoint {
195193
class MyCalculatorEndpoint extends CalculatorEndpoint {}
196194
```
197195

198-
Since `CalculatorEndpoint` is marked as `@doNotGenerate` it will not be exposed on the server. Only `MyCalculatorEndpoint` will be accessible from the client, which provides the inherited `add` methods from its parent class.
196+
Since `CalculatorEndpoint` is marked as `@doNotGenerate`, it will not be exposed on the server and no client class will be generated for it. Only `MyCalculatorEndpoint` will be accessible from the client, which provides the inherited `add` methods from its parent class. Unlike abstract endpoints, when a parent is marked with `@doNotGenerate`, the generated client class will implement the base endpoint class directly rather than extending a generated abstract parent class.
199197

200198
### Overriding endpoint methods
201199

@@ -248,7 +246,7 @@ class AdderEndpoint extends CalculatorEndpoint {
248246
```
249247

250248
Since `CalculatorEndpoint` is `abstract`, it will not be exposed on the server. `AdderEndpoint` inherits all methods from its parent class, but since it opts to hide `subtract` by annotating it with `@doNotGenerate` only the `add` method will be exposed.
251-
Don't worry about the exception in the `subtract` implementation. That is only added to satisfy the Dart compiler – in practice, nothing will ever call this method on `AdderEndpoint`.
249+
Don't worry about the exception in the `subtract` implementation. That is only added to satisfy the Dart compiler – in practice, nothing will ever call this method on `AdderEndpoint`.
252250

253251
Hiding endpoints from a super class is only appropriate in case the parent `class` is `abstract` or annotated with `@doNotGenerate`. Otherwise, the method that should be hidden on the child would still be accessible via the parent class.
254252

@@ -306,3 +304,168 @@ abstract class AdminEndpoint extends Endpoint {
306304
```
307305

308306
Again, just have your custom endpoint extend `AdminEndpoint` and you can be sure that the user has the appropriate permissions.
307+
308+
## Client-side endpoint inheritance
309+
310+
When you use endpoint inheritance on the server, Serverpod generates matching client-side classes that mirror your inheritance hierarchy. This allows you to write type-safe client code that works with abstract endpoint types.
311+
312+
### Abstract endpoint client generation
313+
314+
When you define an abstract endpoint on the server, Serverpod generates an abstract client endpoint class. This is particularly useful for module developers who want to provide base functionality that users can extend.
315+
316+
**Server-side abstract endpoint:**
317+
318+
```dart
319+
import 'package:serverpod/serverpod.dart';
320+
321+
abstract class CalculatorEndpoint extends Endpoint {
322+
Future<int> add(Session session, int a, int b) async {
323+
return a + b;
324+
}
325+
}
326+
```
327+
328+
**Generated client-side abstract class:**
329+
330+
```dart
331+
abstract class EndpointCalculator extends EndpointRef {
332+
EndpointCalculator(EndpointCaller caller) : super(caller);
333+
334+
Future<int> add(int a, int b);
335+
}
336+
```
337+
338+
When you extend this abstract endpoint in your server:
339+
340+
```dart
341+
class MyCalculatorEndpoint extends CalculatorEndpoint {
342+
Future<int> subtract(Session session, int a, int b) async {
343+
return a - b;
344+
}
345+
}
346+
```
347+
348+
The generated client class will extend the abstract client class:
349+
350+
```dart
351+
class EndpointMyCalculator extends EndpointCalculator {
352+
EndpointMyCalculator(EndpointCaller caller) : super(caller);
353+
354+
@override
355+
String get name => 'myCalculator';
356+
357+
@override
358+
Future<int> add(int a, int b) => caller.callServerEndpoint<int>(
359+
'myCalculator',
360+
'add',
361+
{'a': a, 'b': b},
362+
);
363+
364+
Future<int> subtract(int a, int b) => caller.callServerEndpoint<int>(
365+
'myCalculator',
366+
'subtract',
367+
{'a': a, 'b': b},
368+
);
369+
}
370+
```
371+
372+
### Using `getEndpointOfType` for type-safe endpoint access
373+
374+
When working with abstract endpoints, you can use the `getEndpointOfType` method to retrieve concrete endpoint instances by their type. This is especially useful when writing code that depends on abstract endpoint interfaces provided by modules.
375+
376+
```dart
377+
// Get an endpoint by its type
378+
var calculator = client.getEndpointOfType<EndpointCalculator>();
379+
380+
// Now you can call methods defined in the abstract base class
381+
var result = await calculator.add(5, 3);
382+
```
383+
384+
The `getEndpointOfType` method will:
385+
386+
- Return the single endpoint of the requested type if only one exists.
387+
- Throw `ServerpodClientEndpointNotFound` if no endpoint of that type is found.
388+
- Throw `ServerpodClientMultipleEndpointsFound` if multiple endpoints of that type exist.
389+
390+
#### Disambiguating multiple endpoints
391+
392+
If you have multiple concrete implementations of the same abstract endpoint, you can disambiguate by providing the endpoint name:
393+
394+
```dart
395+
// Server-side: Two implementations of the same abstract endpoint
396+
class BasicCalculatorEndpoint extends CalculatorEndpoint {}
397+
class AdvancedCalculatorEndpoint extends CalculatorEndpoint {
398+
Future<int> multiply(Session session, int a, int b) async {
399+
return a * b;
400+
}
401+
}
402+
```
403+
404+
```dart
405+
// Client-side: Specify which implementation you want
406+
var basicCalc = client.getEndpointOfType<EndpointCalculator>('basicCalculator');
407+
var advancedCalc = client.getEndpointOfType<EndpointCalculator>('advancedCalculator');
408+
```
409+
410+
#### Use case: Module-provided abstract endpoints
411+
412+
This pattern is particularly powerful for modules. A module can provide an abstract endpoint that defines an interface, and users of the module can extend it to expose the functionality on their server:
413+
414+
**In a module (e.g., `serverpod_auth`):**
415+
416+
Declare an abstract endpoint with common methods on the server:
417+
418+
```dart
419+
abstract class AuthSessionEndpoint extends Endpoint {
420+
Future<bool> isAuthenticated(Session session) async {
421+
return await session.isUserSignedIn;
422+
}
423+
424+
Future<bool> logout(Session session, {required bool allSessions}) async {
425+
// Implementation...
426+
}
427+
}
428+
```
429+
430+
Write client-side code that depends on the generated abstract type and uses its methods:
431+
432+
```dart
433+
class UserLoggedInWidget extends StatelessWidget {
434+
final Client client;
435+
436+
UserLoggedInWidget({required this.client});
437+
438+
// Will throw if no concrete implementation of the endpoint exists.
439+
EndpointAuthSession get sessionEndpoint =>
440+
client.getEndpointOfType<EndpointAuthSession>();
441+
442+
@override
443+
Widget build(BuildContext context) {
444+
return FutureBuilder<bool>(
445+
future: sessionEndpoint.isAuthenticated(),
446+
builder: (context, snapshot) {
447+
if (snapshot.connectionState == ConnectionState.waiting) {
448+
return CircularProgressIndicator();
449+
} else if (snapshot.hasError) {
450+
return Text('Error: ${snapshot.error}');
451+
} else if (snapshot.hasData && snapshot.data == true) {
452+
return Text('User is logged in');
453+
} else {
454+
return Text('User is not logged in');
455+
}
456+
},
457+
);
458+
}
459+
}
460+
```
461+
462+
**In the user application:**
463+
464+
The user will just have to extend the abstract endpoint to expose it on their server. Then, any client code that depends on the abstract endpoint will work seamlessly, regardless of the concrete class name or location.
465+
466+
```dart
467+
// Extend the module's abstract endpoint to expose it
468+
class SessionEndpoint extends AuthSessionEndpoint {}
469+
```
470+
471+
This approach allows module developers to provide reusable endpoint logic while giving application developers full control over which endpoints are exposed on their server.

0 commit comments

Comments
 (0)