Best Practise for storing api routes?


#1

Hello,

due to I’m not 100% sure how to store the api routes I want to ask the community if someonw has “best practices” with that.
In my solution we have different services with different base paths. So it’s not possible to configure e.g. the aurlia-fetch-client-base-path globally.

Following the approaches I tested:

  • Saving the api routes using aurelia-configuration
    • Positive: I can have all routes (prod, stage and dev ) in one file and the package handles the environments for me
    • Negative: I have all routes (prod, stage and dev) always on each environment where I don’t need them and the files are quite big and confusing (not maintainable).
  • Saving the routes in extra “typescript classes” and use the aurelia-cli environment handling
    • Positive: I have the routes only where I need them and the it’s easy to deal with (maybe having an additional service that provides some functionalities e.g. to provide the full url meanign base-path and route).
    • Negative: The files can only be maintained/configured by developers.
  • Writing some external packages and load them via NPM (e.g. dev-dependency)
    • Positive: That can be combined with a mock service providing some mock data.
    • Negative: In case of errors it’s hard to debug respectively the development is much more difficult. For each route change the external package must be adjusted and pushed.

Can anybody give me a “best practise” or a tip? :slight_smile:
Currently my application is small enough that I can change that. I don’t want to run into problems when the application becomes bigger.


#2
@inject(NewInstance.of(HttpClient))
export class ApiService {
  constructor(http) {
    this.http = http;
    this.http.configure((x) => x.withBaseUrl('api'))
  }
}

#3

And where do you keep the api routes? In a ts-file or any json-file?

How do you seperate the routes for each environment?


#4

To extend on @davismj suggestion you can keep them in multiple “Controller” (If your familiar with .Net) like files following that format within a sub folder of your src like EX: src/services. Most people label them as services, EX: [name].service.ts.

You can then inject it into any view model or component that needs them and do service.getSomething().then(data => { ... }). Most like to return a promise or rxjs subscription from them to handle the async nature of data calls, this separation allows you to do any client side caching you want as well.

Not sure if there are better patterns but that’s what I ended up doing with one service acting as the base service (designed like @davismj example ) and all the rest extend it.


#5

Thank you for the response. The challange here is, that I’m not able to seperate the routes between different environments (e.g. for dev I have only local routes (to mocking json-files).) respectively I have all routes everytime on all environments…

Currently I solved that by using the environments.ts file from aurelia-cli. So depending on the command (e.g. au run --watch --env prod) the environments.ts file only includes the right routes for the respective stage. But I’m not sure if that is a good way…


#6

We went for storing routes in a database. You could use an embedded database, a relational database, a NoSQL database, or anything else. This facilitates instant changes without fuss or associated practices such as checking in/out of error prone code just to change one or more routes and other spaghetti programming whilst development progresses. Also a route administration capability can be easily achieved using one of the view models to edit (add/update/delete) the routes. The table below shows an SQL Server Express example where the (left) navigation, main area (moduleId), and right (eg empty or diary) views are shown.


#7

So you have aurelia connected to a database?
I really like the database table structure. There are many good meta information stored to the route.


#8

This is a first class engineered development by software professionals with many years back end, front end, middleware experience. The current Net Core 2.1 application uses EF Core to access database for business objects too (eg Products, Services, Customers, SKU, Pricing) so we can either do migrations (code first) or reverse engineer (database first). Agile methodology is used throughout so time consuming code writing is kept to the absolute minimum using generics throughout. Aurelia binding is fantastic and forms the basis for all the code. The code base is constantly checked out, reviewed, torn down, rebuilt, tested, checked in.


#9

@lvparkington Interesting, are all those modules/routes self contained bundles as well so they are not loaded in unless requested?


#10

All the routes need working view models and references otherwise the application would grind to a halt with a router or navigation failure. If this happens before the navigation is built there be no navigation menu items to select anything!


#11

I wouldn’t recommend a database to hold your routes. This creates a rigid dependency between compiled application and database row entries. If you adopt this approach you should be generating the row contents of the database during the build process, in which case it begs the question of why you would require a database at all, as you can simply generate the routes in your application anyway.

If you’re asking how to define all of the API URLs used against one or more remote services, as @davismj says, make sure your base URL for your API is configured once against the fetch (or http) client. Then look at using something like NSwagStudio to generate an API client class directly from the API being published at your server end, and make sure your server-API exposes an OpenAPI interface to read this.
We use this approach to expose hundreds of API calls from complex management systems, and as part of the development process we run an NPM script that runs the NSWAG application and generates the typescript, fully-typed, error-wrapped client code that we then call to talk to the server.

To differentiate between environments, this should be part of your deployment process, not build or execution. Ideally your code should be built identically for DEV, UAT, PROD, and when it’s loaded onto a server it’s given the associated configuration data. We always ensure our API is relative to the application’s URL (i.e. under ./api/) so that @davismj’s approach works.
If it’s not possible to make it relative, I would suggest you embed the base API URL in the index.html during deployment (e.g. as an attribute of the app’s DIV), and have the application read that during startup. Again, it means your app is unchanged between environments, otherwise you have to know the API endpoint to be able to ask it where the API endpoint is :slight_smile:


#12

I fully agree to it. In my opinion a frontend (especially a SPA) should always be independent. Exactly this is the advantage of having a web service architecture. Additionally, I think that routes are mostly core data that doesn’t change (shouldn’t) that often.


#13

Each route is a dependency (effectively a module reference) in an Aurelia application and to work correctly, all need to be compiled/bundled as references from code which may be developer written code or automatically generated code. Agreed a database is yet another dependency but the application already uses a database to store other information, subject to slow or rapid change otherwise could be hardcoded as constant, such as customer/product/quotation information and it is quite easy to change a database table (eg to hide a route whilst debugging a customer issue) without checking code out of a repository, editing route generation code (usually in app.ts), testing, checking out, redeploying etc. What would be the best way of automatically generating routes in an application without using data from a database?


#14

I would assume, that routes doesn’t change that often.

Agreed a database is yet another dependency but the application already uses a database to store other information

In our case we have a frontend that gets information from different web services, not only one. I guess this is the main idea of having web services. The webservices itself don’t need to care about routing, frontend etc. Only the frontend needs to know, how to get the needed information.

But this is all a question of responsibilities. Which applications should be responsibile for what :slight_smile: . And following responsibility depends on the desinged architecture.

I like to have that discussion here. Thank you very much.


#15

OK, so I suppose the question is whether the sources of data change between (or even during) application execution, or if they remain static for the lifetime of the application build.
If they can be changed at any time, storing these at the server end and requesting a ‘snapshot’ during initialisation makes sense. This also (almost) bypasses the DEV/UAT/PROD issue as the environment could be chosen either by the authentication mechanism or by a different request.
If they are static to the compilation instance then they could be included as a JSON file that sits in the same source as the app bundle, with the deployment mechanism ensuring the appropriate one for that environment is copied to that location. There’s no risk of the wrong environment being readable, but again, it’s a deployment task, not compilation or runtime.

@lvparkington, I’m not sure I see any benefits to the database held set of routes. API endpoints are configuration items, but routes refer to both views and modules, both of which are hard-coded into your SPA. If the intention was to disable the ‘Tree’ module for users, the configureRouter would simply not add that module as instructed by information returned from the remote API. You may have a table containing activated modules, but I would not recommended including module and view names in that table (particularly relative ones).

You ask how to automatically generate routes, but that’s not possible in an SPA. Routes either exist or they don’t. What we can do is enable or disable routes, which is a common requirement in authorisation. For example, if a Tree module with /tree route is only available if the user is a member of group ‘leaf’, then you exclude that route from the router if it’s not accessible (or even just disable it). The check for membership of ‘leaf’ is the server-side function. If it says ‘yes’, then the route is rendered. You could choose to lazy-load on demand a Tree module after authorisation has been performed, or you may choose to simply include all the modules.
There’s no need for client-side security re the ‘Tree’ module, as every API call should be checking authorisation anyway, and the hiding of the menu is simply a visual nicety, not security-through-obscurity.

Of course there can be all sorts of reasons for doing some very strange things, so I may be missing some other complex reason for implementing that in your solution.


#16

The user requirements are paramount. They see the front end as the application so during their login using a favourite browser, the SPA is configured during start up, using SPA environment (ie package.json ) config files (appsettings.json), databases (eg Identity, Tenant), for personalisation/security reducing clutter so nothing is shown they are not authorised to see. This would includes routes and controls on the displayed page to ensure the best user experience. Generally the fewer warning dialogs the better. Developers in a team need to keep in step and it helps to get as much as possible out of code into configuration files and/or databases (accessed by changing connection strings if necessary). Suppose the boss asks to see a new feature that has not been finished? Enabling/Disabling a route in a database and recompiling is a lot quicker than finding where the route generation file is (app.ts), manually changing it, making a mistake under pressure and becoming fool of the week as the application fails to load with a TypeScript error. Not wanting to let the side down :slight_smile:


#17

Hmm, OK. I’m not sure how any of those things are related to coding module IDs and views into your database, but if it works for your organisation, great :slight_smile:


#18

Are you really using that “generators” for your applications? I had a look into NSwagStudio and into the generated classed (for aurelia). In my opinion the code doesn’t look that good that I would like to use it on PROD. Especially I’m missing the aurelia-validation (ValidationRules foreach model) part.
Aurelia is much more powerful than this generators can provide.


#19

I’m not quite sure what you’re expecting to be generated, but things like validation rules aren’t supposed to be created by a tool like this. This isn’t an application generator, it’s an API interface code generator.
For example, you might get a set of customer accounts by doing something like

  private fetchAccountProjects(account: CustomerAccount) {
    return this.engage
      .projects(account.CustomerAccountId, account.CustomerAccountId)
      .then(projects => this.accountProjects = projects)
      .catch(r => {
        this.log.exception(r);
        return this.accountProjects = [] as Project[];
      });
  }

CustomerAccount is a DTO auto-defined from the API, this.engage.projects(...) is a call generated that wraps the http fetch and returns a fully typed result set. This is unrelated to validation, doesn’t generate forms, but avoids any differences between server and client side DTOs, avoids having to hand-code hundreds of DTOs, and provides a 100% type-checked relationship between your client and your server API.

You might need to look a little further into what this type of tool does. It addresses part of the chain of development, but it has nothing to do with the UI.


#20

Great. I guess I got the point. So, you mean that the generates classes should only build the core/bridge to the API that I can (if needed) extend or adjust by (e.g.) inheritance to add Aurelia validation rules or others?
This sounds really good for me. I love this thought.

Many thanks.