Skip to main content

Getting Started with OpenLift APIs

This guide will help you set up your development environment and make your first successful API calls to the OpenLift platform.

Prerequisites

Before you begin, ensure you have:

  • Development Environment: Flutter, React Native, or web development setup
  • API Access: Valid OpenLift API credentials (contact the development team)
  • GraphQL Knowledge: Basic understanding of GraphQL queries and mutations
  • Authentication Method: JWT tokens or API keys for your application

Quick Setup Checklist

  • Configure GraphQL client with OpenLift endpoint
  • Set up authentication headers and token management
  • Make your first test query (user profile or exercises)
  • Implement error handling for network issues
  • Set up development environment variables

1. API Endpoint Configuration

Production Environment

GraphQL Endpoint: https://api.openlift.app/graphql
WebSocket Endpoint: wss://api.openlift.app/graphql (for subscriptions)

Development Environment

GraphQL Endpoint: https://dev-api.openlift.app/graphql
WebSocket Endpoint: wss://dev-api.openlift.app/graphql

Local Development

GraphQL Endpoint: http://localhost:8000/graphql
WebSocket Endpoint: ws://localhost:8000/graphql

2. GraphQL Client Setup

Flutter (graphql_flutter)

// pubspec.yaml
dependencies:
graphql_flutter: ^5.1.2
flutter_secure_storage: ^9.0.0

// lib/services/graphql_client.dart
import 'package:graphql_flutter/graphql_flutter.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class GraphQLClientService {
static const String _endpoint = 'https://api.openlift.app/graphql';
static const _storage = FlutterSecureStorage();

static GraphQLClient? _client;

static Future<GraphQLClient> getClient() async {
if (_client != null) return _client!;

final httpLink = HttpLink(_endpoint);

// Authentication link
final authLink = AuthLink(
getToken: () async {
final token = await _storage.read(key: 'access_token');
return token != null ? 'Bearer $token' : null;
},
);

// Error handling link
final errorLink = ErrorLink(
errorHandler: (ErrorLinkException exception) {
if (exception.response?.errors?.any(
(error) => error.extensions?['code'] == 'UNAUTHENTICATED'
) == true) {
// Handle token refresh or redirect to login
_handleAuthError();
}
},
);

final link = Link.from([errorLink, authLink, httpLink]);

_client = GraphQLClient(
link: link,
cache: GraphQLCache(store: HiveStore()),
defaultPolicies: DefaultPolicies(
query: Policies(cachePolicy: CachePolicy.cacheFirst),
mutate: Policies(cachePolicy: CachePolicy.networkOnly),
),
);

return _client!;
}

static void _handleAuthError() {
// Implement token refresh or navigation to login
// This will be covered in the Authentication Guide
}
}

React/JavaScript (Apollo Client)

// src/services/apollo-client.js
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';

const httpLink = createHttpLink({
uri: 'https://api.openlift.app/graphql',
});

// Authentication link
const authLink = setContext(async (_, { headers }) => {
const token = localStorage.getItem('access_token');

return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});

// Error handling link
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.forEach(({ message, locations, path, extensions }) => {
if (extensions?.code === 'UNAUTHENTICATED') {
// Handle authentication errors
handleAuthError();
}
});
}

if (networkError) {
console.error(`Network error: ${networkError}`);
}
});

const client = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
errorPolicy: 'all',
},
query: {
errorPolicy: 'all',
},
},
});

export default client;

3. Authentication Setup

Initial Authentication (Flutter)

// lib/services/auth_service.dart
class AuthService {
static const _storage = FlutterSecureStorage();
static const String _accessTokenKey = 'access_token';
static const String _refreshTokenKey = 'refresh_token';

// Login with email and password
Future<AuthResult> login(String email, String password) async {
const String loginMutation = '''
mutation LoginUser(\$input: LoginUserInput!) {
loginUser(input: \$input) {
accessToken
refreshToken
user {
id
email
username
displayName
}
}
}
''';

final client = await GraphQLClientService.getClient();

final result = await client.mutate(
MutationOptions(
document: gql(loginMutation),
variables: {
'input': {
'email': email,
'password': password,
}
},
),
);

if (result.hasException) {
throw AuthException(
_parseAuthError(result.exception!),
);
}

final loginData = result.data!['loginUser'];

// Store tokens securely
await _storage.write(key: _accessTokenKey, value: loginData['accessToken']);
await _storage.write(key: _refreshTokenKey, value: loginData['refreshToken']);

return AuthResult(
success: true,
user: User.fromJson(loginData['user']),
);
}

// Check if user is authenticated
Future<bool> isAuthenticated() async {
final token = await _storage.read(key: _accessTokenKey);
if (token == null) return false;

// Optionally verify token validity
return !_isTokenExpired(token);
}

// Logout and clear tokens
Future<void> logout() async {
await _storage.delete(key: _accessTokenKey);
await _storage.delete(key: _refreshTokenKey);

// Clear GraphQL cache
final client = await GraphQLClientService.getClient();
await client.cache.reset();
}

String _parseAuthError(OperationException exception) {
if (exception.graphqlErrors.isNotEmpty) {
final error = exception.graphqlErrors.first;
switch (error.extensions?['code']) {
case 'INVALID_CREDENTIALS':
return 'Invalid email or password';
case 'USER_NOT_FOUND':
return 'No account found with this email';
case 'ACCOUNT_LOCKED':
return 'Account temporarily locked';
default:
return error.message;
}
}
return 'Login failed. Please try again.';
}

bool _isTokenExpired(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) return true;

final payload = json.decode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[1])))
);

final exp = payload['exp'] as int;
return DateTime.now().millisecondsSinceEpoch / 1000 >= exp;
} catch (e) {
return true;
}
}
}

4. Making Your First API Calls

Test Connectivity with Exercise Query

// lib/services/exercise_service.dart
class ExerciseService {
static Future<void> testConnection() async {
const String testQuery = '''
query TestConnection {
exercises(first: 5) {
totalCount
edges {
node {
id
name
description
}
}
}
}
''';

final client = await GraphQLClientService.getClient();

final result = await client.query(
QueryOptions(
document: gql(testQuery),
cachePolicy: CachePolicy.networkOnly,
),
);

if (result.hasException) {
print('Connection test failed: ${result.exception}');
throw Exception('Failed to connect to OpenLift API');
}

final exercises = result.data!['exercises'];
print('✅ Connection successful! Found ${exercises['totalCount']} exercises');
print('First exercise: ${exercises['edges'][0]['node']['name']}');
}
}

Get User Profile (Authenticated)

// lib/services/user_service.dart
class UserService {
static Future<User> getCurrentUser() async {
const String userQuery = '''
query GetCurrentUser {
userProfile {
id
email
username
displayName
fitnessLevel
unitPreferences {
weightUnit
distanceUnit
}
}
}
''';

final client = await GraphQLClientService.getClient();

final result = await client.query(
QueryOptions(
document: gql(userQuery),
cachePolicy: CachePolicy.cacheFirst,
),
);

if (result.hasException) {
throw UserException('Failed to fetch user profile');
}

return User.fromJson(result.data!['userProfile']);
}
}

5. Error Handling Setup

Global Error Handler

// lib/utils/error_handler.dart
class GlobalErrorHandler {
static void handleGraphQLError(OperationException exception, BuildContext context) {
String message = 'An unexpected error occurred';
String? action;

// Parse GraphQL errors
if (exception.graphqlErrors.isNotEmpty) {
final error = exception.graphqlErrors.first;
final code = error.extensions?['code'];

switch (code) {
case 'UNAUTHENTICATED':
message = 'Please log in to continue';
action = 'LOGIN';
break;
case 'FORBIDDEN':
message = 'You don\'t have permission for this action';
break;
case 'VALIDATION_ERROR':
message = error.message;
break;
case 'RATE_LIMITED':
message = 'Too many requests. Please wait and try again.';
break;
default:
message = error.message;
}
}

// Parse network errors
if (exception.linkException != null) {
if (exception.linkException is NetworkException) {
message = 'Network error. Please check your connection.';
} else if (exception.linkException is ServerException) {
message = 'Server error. Please try again later.';
}
}

// Show user-friendly error
_showErrorDialog(context, message, action);
}

static void _showErrorDialog(BuildContext context, String message, String? action) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('OK'),
),
if (action == 'LOGIN')
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.pushReplacementNamed(context, '/login');
},
child: const Text('Login'),
),
],
),
);
}
}

6. Development Configuration

Environment Configuration

// lib/config/app_config.dart
class AppConfig {
static const String _environment = String.fromEnvironment('ENVIRONMENT', defaultValue: 'development');

static bool get isDevelopment => _environment == 'development';
static bool get isProduction => _environment == 'production';

static String get apiEndpoint {
switch (_environment) {
case 'production':
return 'https://api.openlift.app/graphql';
case 'staging':
return 'https://staging-api.openlift.app/graphql';
default:
return 'http://localhost:8000/graphql';
}
}

static bool get enableDebugLogs => !isProduction;
static bool get enableHMACAuth => isProduction;
}

Debug Logging

// lib/utils/debug_logger.dart
class DebugLogger {
static void logGraphQLOperation(String operationName, Map<String, dynamic>? variables) {
if (!AppConfig.enableDebugLogs) return;

print('🔍 GraphQL Operation: $operationName');
if (variables != null && variables.isNotEmpty) {
print('📝 Variables: ${json.encode(variables)}');
}
}

static void logGraphQLResponse(String operationName, dynamic data, List<GraphQLError>? errors) {
if (!AppConfig.enableDebugLogs) return;

if (errors != null && errors.isNotEmpty) {
print('❌ GraphQL Errors for $operationName:');
for (final error in errors) {
print(' - ${error.message}');
}
} else {
print('✅ GraphQL Success: $operationName');
}
}
}

7. Testing Your Integration

Basic Integration Test

// test/integration/api_integration_test.dart
import 'package:flutter_test/flutter_test.dart';

void main() {
group('OpenLift API Integration', () {
test('should connect to API and fetch exercises', () async {
// Test basic connectivity
await ExerciseService.testConnection();

expect(true, true); // If no exception thrown, test passes
});

test('should handle authentication flow', () async {
// Test login (use test credentials)
final authResult = await AuthService().login(
'test@example.com',
'testPassword123',
);

expect(authResult.success, true);
expect(authResult.user.email, 'test@example.com');

// Test authenticated request
final user = await UserService.getCurrentUser();
expect(user.id, isNotNull);
});

test('should handle errors gracefully', () async {
// Test invalid credentials
expect(
() => AuthService().login('invalid@example.com', 'wrongpassword'),
throwsA(isA<AuthException>()),
);
});
});
}

8. Next Steps

Once you have the basic setup working:

  1. Explore Specific APIs: Check individual service documentation for detailed operations
  2. Implement Authentication Flow: Set up complete login, registration, and token refresh
  3. Add Error Handling: Implement comprehensive error handling for all operations
  4. Set Up Offline Support: Add caching and offline capabilities
  5. Optimize Performance: Implement query optimization and caching strategies

Common Issues & Solutions

Issue: GraphQL Connection Timeout

// Solution: Configure timeout settings
final httpLink = HttpLink(
AppConfig.apiEndpoint,
httpClientOverride: HttpClient()
..connectionTimeout = const Duration(seconds: 10)
..timeout = const Duration(seconds: 30),
);

Issue: Token Refresh Not Working

// Solution: Implement proper token refresh logic
class TokenManager {
static Future<String?> getValidToken() async {
String? token = await _storage.read(key: 'access_token');

if (token == null || _isTokenExpired(token)) {
// Attempt token refresh
token = await _refreshToken();
}

return token;
}

static Future<String?> _refreshToken() async {
final refreshToken = await _storage.read(key: 'refresh_token');
if (refreshToken == null) return null;

// Implement refresh token mutation
// Store new tokens and return access token
}
}

Issue: CORS Issues in Web Development

// Solution: Configure proxy in development
// Create proxy.conf.json for Angular or similar for other frameworks
{
"/graphql/*": {
"target": "https://api.openlift.app",
"secure": true,
"changeOrigin": true
}
}

Resources

  • GraphQL Playground: Test queries interactively at /graphql endpoint
  • API Documentation: Complete schema documentation in this documentation site
  • Sample Code: Reference implementations in the code examples repository
  • Community Support: Developer Discord/Slack channels for real-time help

Next: Continue with the Flutter Integration Guide for mobile-specific patterns or Authentication Guide for security implementation.