I am creating a web app with django rest framework as backend and react as frontend. The react app i generated from create-react-app. To make api call, i use fetch Api. I see some repetition in my api call that need to a reafactor. But i know which pattern is better here.
Here is the code:
Api.js
let _options = {headers: new Headers({'Content-Type': 'application/json'})}
const _url = {
'users': '/api/users/',
'obtain-token': '/api/obtain-token/',
'verify-token': '/api/verify-token/',
'refresh-token': '/api/refresh-token/'
}
const _fetch = (request) => {
return fetch(request)
.then(res => {
if ( ! res.ok) {
let err = {
'status': res.status,
'statusText': res.statusText,
'data': {}
}
return res.json().then(
data => {
err.data = data
return Promise.reject(new Error(JSON.stringify(err)))
},
() => Promise.reject(new Error(JSON.stringify(err)))
)
}
return res.json()
})
.catch(err => {
return Promise.reject(err)
})
}
export const obtainToken = (username, password) => {
const credential = {username, password}
let options = _options
options.method = 'POST'
options.body = JSON.stringify(credential)
const request = new Request(_url['obtain-token'], options)
return _fetch(request)
}
export const verifyToken = (token) => {
let options = _options
options.method = 'POST'
options.body = JSON.stringify({token})
const request = new Request(_url['verify-token'], options)
return _fetch(request)
}
export const refreshToken = (token) => {
let options = _options
options.method = 'POST'
options.body = JSON.stringify({token})
const request = new Request(_url['refresh-token'], options)
return _fetch(request)
}
export const getUser = (username, token='') => {
let options = _options
options.method = 'GET'
if (token) options.headers.append('Authorization', `JWT ${token}`)
const request = new Request(`${_url['users']}/${username}`, options)
return _fetch(request)
}
What i like from this code is, when i need to request a resource, i just run a function with needed parameter without knowing it is get or post or else, and i dont need to pass header configuration and url.
// example api call
let token = '4346h9r7yt47t9...'
verifyToken(token)
.then(data => {
// server response json available here
token = data.token
})
.catch(err => {
// handle network error and bad response here
console.log(err)
})
Im follwing #challenger solution. And manage to get it shorter with this:
// Change _build function name to makeRequest and export it
const makeRequest = (name, _url='', body=undefined, headers={}) => {
let [method, url] = source[name]
let options = {
headers: new Headers({
'Content-Type': 'application/json',
...headers,
})
}
options.method = method
if (body) options.body = JSON.stringify(body)
return _fetch(url+_url, options)
}
export default makeRequest
But, of course to call this function i need to supply the appended url, and a custom header if needed. But as this project goes it's proved that appended url (use by user detail resource) and costum header (use by resource that need authentication) is needed.
let source = {
'getUser': (..._p) => ['GET', `/api/users/${_p[0]}`, undefined, {'Authorization': `JWT ${_p[1]}`}],
'obtainToken': (..._p) => ['POST','/api/obtain-token/', {username:_p[0], password:_p[1]}],
'verifyToken': (..._p) => ['POST','/api/verify-token/', {token:_p[0]}],
'refreshToken': (..._p) => ['POST', '/api/refresh-token/',{token:_p[0]}]
}
const requests = {}
Object.keys(source).forEach(key => {
requests[key] = (...params) => {
let [method, url, body, headers ={}] = source[key](...params)
let options = {
headers: new Headers({
'Content-Type': 'application/json',
...headers,
})
}
options.method = method
if (body) options.body = JSON.stringify(body)
return _fetch(new Request(url, options))
}
})
export default requests
it needs more testing and review..
let source = {
'getUser': ['GET', '/api/users/'],
'obtain-token': ['POST','/api/obtain-token/'],
'verify-token': ['POST','/api/verify-token/'],
'refresh-token':['POST', '/api/refresh-token/']
}
let _build = (name, _url, body, headers) => {
let [method, url] = source[name]
let options = {
headers: new Headers({
'Content-Type': 'application/json',
...headers,
})
}
options.method = method
if (body) options.body = JSON.stringify(body)
return new Request(url+_url, options)
}
and then
export const obtainToken = (username, password) => {
const credential = {username, password}
const request = _build('obtainToken','', credential, {})
return _fetch(request)
}
export const verifyToken = (token) => {
const request = _build('verifyToken', '',{token}, {})
return _fetch(request)
}
export const refreshToken = (token) => {
const request = _build('refreshToken', '', {token}, {})
return _fetch(request)
}
export const getUser = (username, token='') => {
let headers = {'Authorization': `JWT ${token}`}
const request = _build('getUser',`/${username}`, undefined, headers)
return _fetch(request)
}
even more, instead of...
return new Request(url+_url, options)
in the _build function you can replace it with
return _fetch(new Request(url+_url, options))
which allows you to have this:
export const obtainToken = (username, password) => {
const credential = {username, password}
return _build('obtainToken','', credential, {})
}
export const verifyToken = (token) => {
return _build('verifyToken', '',{token}, {})
}
export const refreshToken = (token) => {
retun _build('refreshToken', '', {token}, {})
}
export const getUser = (username, token='') => {
let headers = {'Authorization': `JWT ${token}`}
return _build('getUser',`/${username}`, undefined, headers)
}
Related
const baseQuery = fetchBaseQuery({
baseUrl: 'url',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
// If we have a token set in state, let's assume that we should be passing it.
if (token) {
headers.set('authorization', `Bearer ${token}`)
}
return headers
}
})
const baseQueryWithReauth = async (args, api, extraOptions) => {
let result = await baseQuery(args, api, extraOptions)
if (result?.error?.originalStatus === 401) {
//I noticed result.error.originalStatus always returned undefine
console.log('sending refresh token')
const refreshResult = await baseQuery({
url: '/users/generateTokens',
method: 'POST'
}, api, extraOptions)
console.log(refreshResult)
if (refreshResult?.data) {
const user = api.getState().auth.user
api.dispatch(setCredits({ ...refreshResult.data, user }))
result = await baseQuery(args, api, extraOptions)
console.log(result)
} else {
api.dispatch(logOut())
}
}
return result
}
So, my knowledge of next and JS in general is extremely minimal as is my Auth0 knowledge. I was wondering if anyone could suggest how to pass query parameters or a request body to the first block of code below?
I know I can add a body to the fetch function, but when I try to do that in the /pages/profile block below, it seems to break the request.
Even better would be to make this some kind of generic function, where I can pass in a the route, method, and body since all of the routes will be protected anyway.
Any help would be greatly appreciated.
/pages/api/my/user
import { getAccessToken, withApiAuthRequired } from '#auth0/nextjs-auth0';
export default withApiAuthRequired(async function shows(req, res) {
try {
const { accessToken } = await getAccessToken(req, res, {
scopes: ['profile']
});
const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/v1/my/user`, {
headers: {
Authorization: `Bearer ${accessToken}`
},
});
const shows = await response.json();
res.status(200).json(shows);
} catch (error) {
res.status(error.status || 500).json({ error: error.message });
}
});
And here's the snippet that fetches the above:
/pages/profile
const [state, setState] = useState({ isLoading: false, response: undefined, error: undefined });
useEffect(() => {
callApi();
}, []);
const callApi = async () => {
setState(previous => ({ ...previous, isLoading: true }))
try {
const response = await fetch(`/api/my/user`);
const data = await response.json();
setState(previous => ({ ...previous, response: data, error: undefined }))
} catch (error) {
setState(previous => ({ ...previous, response: undefined, error }))
} finally {
setState(previous => ({ ...previous, isLoading: false }))
}
};
const { isLoading, response, error } = state;
Cannot see where your actual problem is - here's a snippet that usually works for me with fetch:
provide headers and body as parameters of an options variable, add the url and you are good to go.
const res = await fetch(url, options)
const protocol = 'https'
const hostname = 'YOURHOSTNAME'
const queryParams = `foo=bar`
const body = []
const url = `${protocol}://${hostname}/api/${endpoint}?${queryParams}`;
const options = {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': `${hostname}`, // set as needed
},
body: body,
method: 'POST',
};
const res = fetch(url, options);
Hope this helps
I'm working on a project in codecademy that lets you make a playlist through spotify, but when I type something in the search nothing pops out.I assume its something with the spotify api.
I was seeing that spotify changed their endpoints but I'm not really sure. Nothing in the console says any error. Any help?
Spotify.js
const clientId = '**';
const redirectUri = 'http://localhost:3000'
let accessToken;
const Spotify = {
getAccessToken() {
if (accessToken){
return accessToken;
}
//check for access token match
const accessTokenMatch = window.location.href.match(/access_token=([^&]*)/);
const expiresInMatch = window.location.href.match(/expires_in=([^&]*)/);
if(accessTokenMatch && expiresInMatch) {
accessToken = accessTokenMatch[1];
const expiresIn = Number(expiresInMatch[1]);
//clears params, allowing to grab new access token when it expires
window.setTimeout(() => accessToken ='', expiresIn * 1000);
window.history.pushState('Access Token', null, '/');
return accessToken;
} else {
const accessUrl = `https://accounts.spotify.com/authorize?client_id=${clientId}&response_type=token&scope=playlist-modify-public&redirect_uri=${redirectUri}`;
window.location = accessUrl;
}
},
search(term){
const accessToken = Spotify.getAccessToken();
return fetch(`https://api.spotify.com/v1/search?type=track&q=${term}`, {headers: {
Authorization: `Bearer ${accessToken}`
}
}).then(response => {
return response.json();
}).then(jsonResponse => {
if (!jsonResponse.tracks){
return [];
}
return jsonResponse.tracks.items.map(track => ({
id: track.id,
name: track.name,
artist: track.artist[0].name,
album: track.album.name,
uri: track.uri
}))
})
},
savePlayList(name, trackUris){
if(!name || !trackUris.length){
return;
}
const accessToken = Spotify.getAccessToken();
const headers = { Authorization: `Bearer ${accessToken}`};
let userId;
return fetch('https://api.spotify.com/v1/me', {headers: headers}
).then(response => response.json()
).then(jsonResponse => {
userId = jsonResponse.id;
return fetch(`https://api.spotify.com/v1/users/${userId}/playlists`,
{
headers: headers,
method: 'POST',
body: JSON.stringify({name: name})
}).then(response => response.json()
).then(jsonResponse => {
const playListId = jsonResponse.id;
return fetch(`https://api.spotify.com/v1/users/${userId}/playlists/${playListId}/tracks`, {
headers: headers,
method: 'POST',
body: JSON.stringify({uris: trackUris})
})
})
})
}
}
export default Spotify;
I found that spotify api changed the json response. I removed the [0] in artist: track.artist[0].name to artist: track.artists.name and this worked.
I have made a wrapper for fetch function for my API calls in react-native. I dont want to pass JWT token everytime that I make an API call, so I thought that fetching it inside wrapper will fix it for me, but I cannot get it to work because of async nature...
useFetch.js
// import qs from "querystring";
import { getUserAuthToken } from "../storage";
const responseChecker = async (response) => {
let error = "";
let data = {};
let statusCode = null;
if (!response.ok) {
error = "Something went wrong";
statusCode = response.status;
} else {
statusCode = response.status;
data = await response.json();
}
return { statusCode, error, data };
};
const fetchAuthToken = getUserAuthToken();
const useFetch = (baseURL, authHeader = null) => {
console.log(fetchAuthToken);
**//Cannot get this token in time for the API call ^**
const defaultHeader = {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
key: "1c419c7e-3a34-49f0-9192-b48d4534dff3",
Authorization: authHeader ? authHeader :fetchAuthToken,
};
const customFetch = (
url,
method = "GET",
body = false,
headers = defaultHeader,
) => {
const options = {
method,
headers,
credentials: "include",
};
if (body) options.body = body;
return fetch(url, options);
};
const get = async (endpoint) => {
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "GET");
return responseChecker(response);
};
const post = async (endpoint, body = {}) => {
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "POST", body);
return responseChecker(response);
};
const put = async (endpoint, body = {}) => {
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "PUT", body);
return responseChecker(response);
};
return {
get,
post,
put,
};
};
export default useFetch;
storage.js
import AsyncStorage from "#react-native-community/async-storage";
export const getUserAuthToken = async () => {
try {
const userToken = await AsyncStorage.getItem("userAuthToken");
return userToken;
} catch (e) {
console.log("error");
}
};
exportAPI.js
import useFetch from "./fetch";
const LOCAL_IP = "192.168.0.131";
export const authAPI = (header) => useFetch(`http://${LOCAL_IP}:8000`, header);
export const activityAPI = useFetch(`http://${LOCAL_IP}:8000`);
Steps.js
import React, { useEffect, useState } from "react";
import { Text, Platform } from "react-native";
import { CardXLarge } from "../../../components/Cards/";
import fitnessKitApis from "../../../utilities/fitnessKits";
import { activityAPI } from "../../../utilities/apis";
const StepCard = (props) => {
const fetchStepsFromFitnessKits = async () => {
if (Platform.OS === "android") {
await fitnessKitApis.historicSteps().then((res) => {
setSteps(res);
});
} else {
await fitnessKitApis.historicSteps((result) => setSteps(result));
}
};
const [steps, setSteps] = useState(0);
useEffect(() => {
fetchStepsFromFitnessKits();
const requestParams = { date: new Date(), steps };
const { data, statusCode, error } = activityAPI.get(
"/v1/user/steps/",
requestParams,
);
// console.log(data, statusCode, error);
}, [steps]);
return (
<CardXLarge>
<Text>{steps}</Text>
</CardXLarge>
);
};
export default StepCard;
I know I can pass authHeader from component but that will result in adding 2-3 lines of code in every component which is not super convenient.
Thanks
If you don't want to use async/await in a function to get items from asyncStorage. You can use either callback or promise in place of async/await.
Callback:
AsyncStorage.getItem('data1', (error, data1) => {
// perform your logic here.
});
Promise:
AsyncStorage.getItem('data1').then(data1=>{
// perform your logic here.
}).catch(error=>{
// handle error
})
I have personally used callback for getItem and worked perfectly. I have not tested the promise version but I expect to do the same work as callback.
...
const fetchAuthToken = getUserAuthToken();
...
Your getUserAuthToken is an asynchronous function and here it is not being awaited. To guarantee that asynchronous call is finished you have to await it or use callbacks as #HungrySoul suggested.
You can't await something outside of an asynchronous function.
Solution that I would suggest is creating a class UseFetch and passing the arguments through the constructor. The argument being here the JWT token that you are getting from the AsyncStorage.
Also, another thing that can be done and is a good practice - use redux for managing the state and keeping the JWT token. You might look into that. It will take a bit longer but it will make your code more elegant.
Edit: Or, you might try something like this.
Keep in mind that you have to wait for promises to resolve before you use what was promised.
Here we are using a closure. You will have to pass an argument (which is an async function) to the useFetchBuilder. That function will be awaited and provide the JWT. You can use the getUserAuthToken for that purpose.
Keep in mind that you have to use await or wait for the promise to resolve before using this function. Problem might be somewhere else in your code - maybe the life cycle methods.
I hope this helped.
const responseChecker = async (response) => {
let error = "";
let data = {};
let statusCode = null;
if (!response.ok) {
error = "Something went wrong";
statusCode = response.status;
} else {
statusCode = response.status;
data = await response.json();
}
return { statusCode, error, data };
};
const useFetchBuilder = async (userTokenProvider) => {
const userToken = await userTokenProvider();
return (baseURL, authHeader = null) => {
const defaultHeader = {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
key: "1c419c7e-3a34-49f0-9192-b48d4534dff3",
Authorization: authHeader ? authHeader : userToken,
};
const customFetch = (
url,
method = "GET",
body = false,
headers = defaultHeader,
) => {
const options = {
method,
headers,
credentials: "include",
};
if (body) options.body = body;
return fetch(url, options);
};
const get = async (endpoint) => {
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "GET");
return responseChecker(response);
};
const post = async (endpoint, body = {}) => {
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "POST", body);
return responseChecker(response);
};
const put = async (endpoint, body = {}) => {
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "PUT", body);
return responseChecker(response);
};
return {
get,
post,
put,
};
};
}
export default useFetchBuilder;
I moved getUserAuthToken function down to each request method function in useFetch function where I can await for the response. Then it all worked perfectly.. Also I could have use getUserAuthToken but usingAsyncStorage.getItem seems much cleaner
modified fetch.js
// import qs from "querystring";
import AsyncStorage from "#react-native-community/async-storage";
const responseChecker = async (response) => {
let error = "";
let data = {};
let statusCode = null;
if (!response.ok) {
error = "Something went wrong";
statusCode = response.status;
} else {
statusCode = response.status;
data = await response.json();
}
return { statusCode, error, data };
};
const useFetch = (baseURL, authHeader = null) => {
const defaultHeader = {
Accept: "application/json",
// "Content-Type": "application/x-www-form-urlencoded",
"Content-Type": "application/json",
key: "1c419c7e-3a34-49f0-9192-b48d4534dff3",
Authorization: authHeader,
};
const customFetch = (
url,
method = "GET",
body = false,
headers = defaultHeader,
) => {
const options = {
method,
headers,
credentials: "include",
};
if (body) options.body = JSON.stringify(body);
return fetch(url, options);
};
const get = async (endpoint) => {
await AsyncStorage.getItem("userAuthToken").then((result) => {
defaultHeader.Authorization = result;
});
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "GET");
return responseChecker(response);
};
const post = async (endpoint, body = {}) => {
await AsyncStorage.getItem("userAuthToken").then((result) => {
defaultHeader.Authorization = result;
});
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "POST", body);
return responseChecker(response);
};
const put = async (endpoint, body = {}) => {
await AsyncStorage.getItem("userAuthToken").then((result) => {
defaultHeader.Authorization = result;
});
const url = `${baseURL}${endpoint}`;
const response = await customFetch(url, "PUT", body);
return responseChecker(response);
};
return {
get,
post,
put,
};
};
export default useFetch;
Hello after setup a simple async function with promise return i'd like to use then promise instead of try!
But is returning
await is a reserved word
for the second await in the function.
i've tried to place async return promise the data! but did not worked either
async infiniteNotification(page = 1) {
let page = this.state.page;
console.log("^^^^^", page);
let auth_token = await AsyncStorage.getItem(AUTH_TOKEN);
fetch(`/notifications?page=${page}`, {
method: "GET",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Access: auth_token
},
params: { page }
})
.then(data => data.json())
.then(data => {
var allData = this.state.notifications.concat(data.notifications);
this.setState({
notifications: allData,
page: this.state.page + 1,
});
let auth_token = await AsyncStorage.getItem(AUTH_TOKEN);
fetch("/notifications/mark_as_read", {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
Access: auth_token
},
body: JSON.stringify({
notification: {
read: true
}
})
}).then(response => {
this.props.changeNotifications();
});
})
.catch(err => {
console.log(err);
});
}
> await is a reserved word (100:25)
let auth_token = await AsyncStorage.getItem(AUTH_TOKEN);
^
fetch("/notifications/mark_as_read", {
You should refactor how you make your requests. I would have a common function to handle setting up the request and everything.
const makeRequest = async (url, options, auth_token) => {
try {
// Default options and request method
if (!options) options = {}
options.method = options.method || 'GET'
// always pass a body through, handle the payload here
if (options.body && (options.method === 'POST' || options.method === 'PUT')) {
options.body = JSON.stringify(options.body)
} else if (options.body) {
url = appendQueryString(url, options.body)
delete options.body
}
// setup headers
if (!options.headers) options.headers = {}
const headers = new Headers()
for(const key of Object.keys(options.headers)) {
headers.append(key, (options.headers as any)[key])
}
if (auth_token) {
headers.append('Access', auth_token)
}
headers.append('Accept', 'application/json')
headers.append('Content-Type', 'application/json')
options.headers = headers
const response = await fetch(url, options as any)
const json = await response.json()
if (!response.ok) {
throw json
}
return json
} catch (e) {
console.error(e)
throw e
}
}
appendQueryString is a little helper util to do the get qs params in the url
const appendQueryString = (urlPath, params) => {
const searchParams = new URLSearchParams()
for (const key of Object.keys(params)) {
searchParams.append(key, params[key])
}
return `${urlPath}?${searchParams.toString()}`
}
Now, to get to how you update your code, you'll notice things become less verbose and more extensive.
async infiniteNotification(page = 1) {
try {
let auth_token = await AsyncStorage.getItem(AUTH_TOKEN);
const data = await makeRequest(
`/notifications`,
{ body: { page } },
auth_token
)
var allData = this.state.notifications.concat(data.notifications);
this.setState({
notifications: allData,
page: this.state.page + 1,
});
const markedAsReadResponse = makeRequest(
"/notifications/mark_as_read",
{
method: "POST",
body: {
notification: { read: true }
},
auth_token
)
this.props.changeNotifications();
} catch (e) {
// TODO handle your errors
}
}