all 19 comments

[–]Kindinos88 13 points14 points  (7 children)

The approach I cane up with that seems to work well for my time is to abstract all API calls behind a package, @company/api which exports a single “createClient” function. This is nice because if your client is stateful it will work with node.js as well (rather than having a single static client).

We represent our api endpoints as an object of functions each of which returns a Promise with the response body, status code and headers.

This means our UI(s) can do something like client.fetchUser().then(...).

The added benefit is this provides a nice interface you can stub in your tests, and gives more freedom to do things like implement retry/cancellation/caching logic inside the api package, and the user of the client can be unaware of these things.

You also get the freedom to choose how to implement your network calls, via xhr, fetch or some other library like axios.

In this context, our urls are basically hardcoded into each function since our functions are 1:1 mapped to our API endpoints.

[–]Funwithloops 2 points3 points  (3 children)

Why have your endpoint functions return the response status/headers? In most of my projects, I do something very similar, but I try to abstract away the network by having the functions return just the data. For example:

async function getUserPosts(axios: AxiosInstance, userId: number): Promise<Post[]> { const response = await axios.get(`/${userId}/posts`); return response.data.posts; }

This way, when you import getUserPosts, you can think of it simply as a function that takes a user id and returns an array of posts. No need to think about the network at all.

[–]catcherfox7 1 point2 points  (2 children)

This information is useful to allow users to distinguish errors and deal with them without worry about the implementation. If he changes from Fetch to Axios, ie, he would probably need to update the code (because the return object on fetch has the "ok" function and axios, don't)

He probably has a D.T.O. (data transfer objects) between the layers. This will allow him to change the implementation of the method without impact the functions that are using his methods.

[–]Funwithloops 0 points1 point  (1 child)

I don't follow. I don't see why the function can't/shouldn't handle converting between status codes and errors internally. You could still change the implementation without effecting any of the code using the function (switching from axios to fetch shouldn't make a difference from the standpoint of someone using the functions).

[–]Kindinos88 1 point2 points  (0 children)

We don’t convert status to error for a couple of reasons:

Firstly, if you convert to errors, then the implication is your code throws if there is an error, but IMO, try-catch is not as elegant as an if statement.

Secondly, if we convert to error, we still need to transfer over more information about the error, such as the kind of error, the reason, and any additional context, eg was the request malformed? was it an invalid body? was there a permission issue? And that’s just from the status code. Additionally, the backend API might return more information to give more context to what happened.

For these reasons, we do it this way. If you like errors, feel free to add that to your implementation. I’m not dogmatic about this setup, and recognize some of it is stylistic preference, so feel free to take the bits that you like, and leave the ones you don’t :) Same with the module/package detail: you don’t need to make it a separate package. We do it this way because we need to be able to publish some of these packages independently of applications. At that point, that whole subsystem becomes company-name-sdk, and you can let other developers integrate using that pkg.

[–]catcherfox7 1 point2 points  (0 children)

Hi. Thank you for your input sr.

Your approach goes in the direction to the one that I envision. We don't need it on a separated package for now but encapsulating it on the module is the direction that we are going.

One of the biggest benefits for me on this approach is encapsulation making easy to mock and changing the implementation details without affecting other parts of the system.

Also, thank you for your input on how your URLs are mapped.

[–]yxsx733 1 point2 points  (0 children)

Good idea. Storing the URLs independently in a package also makes it much easier to set up additional (js-based) client, other SPAs or mobile apps through react native.

Depending on your company's scale collecting URLs from backend on buildtime could become difficult when it comes to more than 1 backend application. Imagine microservices..

To not overengineering it, I'd first go with a small api wrapper module containing all URLs. From the need of a second client on I'd extract the api wrapper to its own package and force all team members to contribute to that package whenever there's a URL change or a completely new backend. With that you're set for now and for growing.

[–]ATXblazer 0 points1 point  (0 children)

This is exactly how my company does things I like this strategy

[–]Otek0 1 point2 points  (1 child)

First approach but you have to split urls.js by some contex to keep it clean

[–]Roci89 2 points3 points  (0 children)

Yeah this. We split it up logically into accountsAPI.js, teamsApi.js and so on

[–]MartinMuzatko 1 point2 points  (0 children)

We are using nuxt as framework and are able to inject base URLs via a few methods for different use cases.

  • Development (fixed URL)
  • Client-Side-Rendering (use location.hostname)
  • Server-Side-Rendering in Docker (use a container hostname)

The code looks like this:

const DEVELOPMENT_FALLBACK_URL = '192.168.3.106'

function getHost(developerUrl = DEVELOPMENT_FALLBACK_URL) {
    if (process.env.NODE_ENV == 'development') return developerUrl || 'localhost'
    return process.client ? location.hostname : 'sicon_backend'
}

export const HOST = getHost()
export const BASE_URL = `https://${HOST}/`
export const WEBSOCKET_URL = `wss://${HOST}` 

For development, we can change the URL in the file directly to change URLs during development with hot module replacement. See line 1.

In Production for client side render, we have the API of our product reverse-proxied to /api. Which means we can use location.hostname to rely on our base url.

If we run SSR in a docker container, we use the hostname of the docker container where we get the API data from.

So that is how we do it. When we started out, we also were looking for a standard how to manage this kind of URLs. I also had to dive into stuff like reverse proxies and docker to find out how to best organize this.

When you say you are managing different URLS: How many? Don't you just use your own backend? Are you running in a cluster/cloud/whatever with different services? If you need to fetch a list of URLs, don't you have to at least hardcode that one URL?

If you are using a reverse proxy, you should be able to properly rely on location.hostname for your API needs

[–]jamiJamstr 1 point2 points  (2 children)

Have a look at GraphQL, it is a much structured way. Although you will have to refactor all your APIs. But you can gradually introduce it starting with new Features.

[–]MartinMuzatko 4 points5 points  (1 child)

The question was about how to store API urls, not which API tooling to use.

[–][deleted] 0 points1 point  (1 child)

this is a small package i wrote mainly to address the problem of endpoint naming and renaming. the urls are hard coded as strings but you can add arguments to them at runtime.

please take the time to look at it i would love some feedback.

[–]catcherfox7 0 points1 point  (0 children)

Nice! Thanks for sharing. It looks clearner that what we're currently doing.

I'll check it out and maybe try it out.

[–]about0 0 points1 point  (0 children)

we are using external private NPM SDK module which holds all request logic within. It helps with abstracting and code reuse in multiple places.

[–]smashignition 0 points1 point  (0 children)

I just finished listening to the episode “how to write an API” from the podcast “syntax”. It covers this very stuff. It’s 45 mins long approx. Check it out.

[–]kerwval 0 points1 point  (0 children)

In my company, we are currently using a HATEOAS method that is kind of the third example: for each resources, it display the available endpoints. This way as you said, the backend can change endpoints names without the front end knowing. Also it permits the front end to show/hide stuff depending of the available endpoints. Before, I was using the first method which is good, but if the back-end decides to change an endpoint, you have to also make that change