Appwrite Backend in Java: Complete Integration Guide

Appwrite is an open-source backend server that provides REST APIs for authentication, databases, storage, and more. This guide covers comprehensive Java integration with Appwrite's various services.


Overview and Architecture

Appwrite Core Services:

  • Authentication: User management, OAuth, JWT
  • Database: Collections, documents, queries
  • Storage: File uploads, downloads, previews
  • Functions: Serverless functions execution
  • Teams & Permissions: Access control management

Java Integration Approach:

  • HTTP Client: REST API communication
  • Object Mapping: JSON serialization/deserialization
  • Error Handling: Comprehensive exception management
  • Security: Secure credential management

Dependencies and Setup

Maven Dependencies
<properties>
<spring-boot.version>3.1.0</spring-boot.version>
<okhttp.version>4.11.0</okhttp.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>logging-interceptor</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
<!-- JWT Support -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
Application Configuration
# application.yml
appwrite:
endpoint: ${APPWRITE_ENDPOINT:http://localhost:8081/v1}
project-id: ${APPWRITE_PROJECT_ID:your-project-id}
api-key: ${APPWRITE_API_KEY:your-api-key}
database-id: ${APPWRITE_DATABASE_ID:your-database-id}
# Service-specific configurations
users:
collection-id: ${USERS_COLLECTION_ID:users}
products:
collection-id: ${PRODUCTS_COLLECTION_ID:products}
orders:
collection-id: ${ORDERS_COLLECTION_ID:orders}
storage:
bucket-id: ${STORAGE_BUCKET_ID:files}
# Security
jwt-secret: ${APPWRITE_JWT_SECRET:your-jwt-secret}
session-duration: 86400 # 24 hours
# HTTP Client
client:
connect-timeout: 10000
read-timeout: 30000
write-timeout: 30000
retry:
enabled: true
max-attempts: 3
backoff-ms: 1000
logging:
level:
com.example.appwrite: DEBUG
Configuration Properties
@Configuration
@ConfigurationProperties(prefix = "appwrite")
@Data
public class AppwriteProperties {
private String endpoint;
private String projectId;
private String apiKey;
private String databaseId;
private String jwtSecret;
private long sessionDuration = 86400;
private ServiceConfig users = new ServiceConfig();
private ServiceConfig products = new ServiceConfig();
private ServiceConfig orders = new ServiceConfig();
private ServiceConfig storage = new ServiceConfig();
private ClientConfig client = new ClientConfig();
@Data
public static class ServiceConfig {
private String collectionId;
private String bucketId;
}
@Data
public static class ClientConfig {
private long connectTimeout = 10000;
private long readTimeout = 30000;
private long writeTimeout = 30000;
private RetryConfig retry = new RetryConfig();
@Data
public static class RetryConfig {
private boolean enabled = true;
private int maxAttempts = 3;
private long backoffMs = 1000;
}
}
}

Core Appwrite Client

1. HTTP Client Configuration
@Configuration
@Slf4j
public class AppwriteClientConfig {
private final AppwriteProperties properties;
public AppwriteClientConfig(AppwriteProperties properties) {
this.properties = properties;
}
@Bean
public OkHttpClient appwriteHttpClient() {
OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(Duration.ofMillis(properties.getClient().getConnectTimeout()))
.readTimeout(Duration.ofMillis(properties.getClient().getReadTimeout()))
.writeTimeout(Duration.ofMillis(properties.getClient().getWriteTimeout()))
.addInterceptor(new AppwriteAuthInterceptor(properties))
.addInterceptor(new LoggingInterceptor())
.addInterceptor(new RetryInterceptor(properties.getClient().getRetry()));
return builder.build();
}
@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());
return mapper;
}
}
@Component
@Slf4j
class AppwriteAuthInterceptor implements Interceptor {
private final AppwriteProperties properties;
public AppwriteAuthInterceptor(AppwriteProperties properties) {
this.properties = properties;
}
@Override
public Response intercept(Chain chain) throws IOException {
Request originalRequest = chain.request();
Request.Builder builder = originalRequest.newBuilder()
.header("X-Appwrite-Project", properties.getProjectId())
.header("X-Appwrite-Key", properties.getApiKey())
.header("Content-Type", "application/json")
.header("User-Agent", "Appwrite-Java-Client/1.0.0");
// Add JWT token if available
String jwtToken = AppwriteSessionContext.getCurrentToken();
if (jwtToken != null) {
builder.header("X-Appwrite-JWT", jwtToken);
}
Request authenticatedRequest = builder.build();
return chain.proceed(authenticatedRequest);
}
}
@Component
@Slf4j
class LoggingInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
long startTime = System.nanoTime();
log.debug("--> {} {}", request.method(), request.url());
if (request.body() != null && log.isDebugEnabled()) {
Buffer buffer = new Buffer();
request.body().writeTo(buffer);
log.debug("Request Body: {}", buffer.readUtf8());
}
Response response = chain.proceed(request);
long endTime = System.nanoTime();
log.debug("<-- {} {} ({}ms)", 
response.code(), response.message(), 
(endTime - startTime) / 1_000_000);
return response;
}
}
@Component
@Slf4j
class RetryInterceptor implements Interceptor {
private final AppwriteProperties.ClientConfig.RetryConfig retryConfig;
public RetryInterceptor(AppwriteProperties.ClientConfig.RetryConfig retryConfig) {
this.retryConfig = retryConfig;
}
@Override
public Response intercept(Chain chain) throws IOException {
if (!retryConfig.isEnabled()) {
return chain.proceed(chain.request());
}
IOException lastException = null;
for (int attempt = 1; attempt <= retryConfig.getMaxAttempts(); attempt++) {
try {
Response response = chain.proceed(chain.request());
// Only retry on server errors (5xx) or network errors
if (response.code() < 500) {
return response;
}
response.close();
if (attempt < retryConfig.getMaxAttempts()) {
log.warn("Request failed with status {}, retrying... (attempt {}/{})", 
response.code(), attempt, retryConfig.getMaxAttempts());
Thread.sleep(retryConfig.getBackoffMs() * attempt);
}
} catch (IOException e) {
lastException = e;
if (attempt < retryConfig.getMaxAttempts()) {
log.warn("Request failed with exception, retrying... (attempt {}/{})", 
attempt, retryConfig.getMaxAttempts(), e);
try {
Thread.sleep(retryConfig.getBackoffMs() * attempt);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", ie);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Retry interrupted", e);
}
}
throw lastException != null ? lastException : new IOException("Max retry attempts exceeded");
}
}
@Component
public class AppwriteSessionContext {
private static final ThreadLocal<String> currentToken = new ThreadLocal<>();
public static void setCurrentToken(String token) {
currentToken.set(token);
}
public static String getCurrentToken() {
return currentToken.get();
}
public static void clear() {
currentToken.remove();
}
}
2. Base Appwrite Service
@Service
@Slf4j
public class AppwriteService {
protected final OkHttpClient httpClient;
protected final ObjectMapper objectMapper;
protected final AppwriteProperties properties;
protected final String baseUrl;
public AppwriteService(OkHttpClient httpClient, 
ObjectMapper objectMapper,
AppwriteProperties properties) {
this.httpClient = httpClient;
this.objectMapper = objectMapper;
this.properties = properties;
this.baseUrl = properties.getEndpoint();
}
protected <T> T executeRequest(Request request, Class<T> responseType) throws AppwriteException {
try {
Response response = httpClient.newCall(request).execute();
return handleResponse(response, responseType);
} catch (IOException e) {
log.error("Request execution failed: {} {}", request.method(), request.url(), e);
throw new AppwriteException("Request failed: " + e.getMessage(), e);
}
}
protected <T> T handleResponse(Response response, Class<T> responseType) throws AppwriteException {
try {
String responseBody = response.body().string();
if (!response.isSuccessful()) {
handleErrorResponse(response, responseBody);
}
if (responseType == Void.class || responseType == void.class) {
return null;
}
return objectMapper.readValue(responseBody, responseType);
} catch (IOException e) {
throw new AppwriteException("Failed to process response: " + e.getMessage(), e);
}
}
protected void handleErrorResponse(Response response, String responseBody) throws AppwriteException {
try {
AppwriteError error = objectMapper.readValue(responseBody, AppwriteError.class);
throw new AppwriteException(
error.getMessage(), 
error.getCode(), 
response.code(),
error.getType()
);
} catch (IOException e) {
throw new AppwriteException(
"HTTP " + response.code() + ": " + responseBody,
"unknown",
response.code(),
"unknown"
);
}
}
protected HttpUrl buildUrl(String path, Map<String, String> queryParams) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl + path).newBuilder();
if (queryParams != null) {
queryParams.forEach(urlBuilder::addQueryParameter);
}
return urlBuilder.build();
}
protected Request.Builder buildRequest() {
return new Request.Builder();
}
protected String toJson(Object object) throws AppwriteException {
try {
return objectMapper.writeValueAsString(object);
} catch (IOException e) {
throw new AppwriteException("Failed to serialize object to JSON", e);
}
}
}
@Data
class AppwriteError {
private String message;
private String code;
private String type;
private int status;
private String version;
}
public class AppwriteException extends RuntimeException {
private final String errorCode;
private final int httpStatus;
private final String errorType;
public AppwriteException(String message) {
super(message);
this.errorCode = "unknown";
this.httpStatus = 500;
this.errorType = "unknown";
}
public AppwriteException(String message, Throwable cause) {
super(message, cause);
this.errorCode = "unknown";
this.httpStatus = 500;
this.errorType = "unknown";
}
public AppwriteException(String message, String errorCode, int httpStatus, String errorType) {
super(message);
this.errorCode = errorCode;
this.httpStatus = httpStatus;
this.errorType = errorType;
}
public boolean isClientError() {
return httpStatus >= 400 && httpStatus < 500;
}
public boolean isServerError() {
return httpStatus >= 500;
}
}

Authentication Service

1. Authentication Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private String id;
private String name;
private String email;
private String phone;
private boolean emailVerification;
private boolean phoneVerification;
private boolean mfa;
private String status;
private String password;
private String passwordUpdate;
private String registration;
private String emailVerificationToken;
private String phoneVerificationToken;
private String mfaRecoveryCode;
private String mfaSecret;
private String prefs;
private String[] targets;
private String accessedAt;
@JsonIgnore
public boolean isActive() {
return "active".equals(status);
}
@JsonIgnore
public boolean isEmailVerified() {
return emailVerification;
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Session {
private String id;
private String userId;
private String expire;
private String provider;
private String providerUid;
private String providerAccessToken;
private String providerAccessTokenExpiry;
private String providerRefreshToken;
private String ip;
private String osCode;
private String osName;
private String osVersion;
private String clientType;
private String clientCode;
private String clientName;
private String clientVersion;
private String clientEngine;
private String clientEngineVersion;
private String deviceName;
private String deviceBrand;
private String deviceModel;
private String countryCode;
private String countryName;
private boolean current;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class LoginRequest {
@Email
private String email;
@NotBlank
private String password;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RegisterRequest {
@NotBlank
private String name;
@Email
private String email;
@NotBlank
private String password;
private Map<String, Object> preferences;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AuthResponse {
private User user;
private Session session;
private String token;
private String refreshToken;
private Instant expiresAt;
}
2. Authentication Service Implementation
@Service
@Slf4j
public class AppwriteAuthService extends AppwriteService {
private static final String AUTH_PATH = "/account";
public AppwriteAuthService(OkHttpClient httpClient, 
ObjectMapper objectMapper,
AppwriteProperties properties) {
super(httpClient, objectMapper, properties);
}
public User register(RegisterRequest request) throws AppwriteException {
log.info("Registering new user: {}", request.getEmail());
try {
Map<String, Object> body = new HashMap<>();
body.put("userId", "unique()");
body.put("email", request.getEmail());
body.put("password", request.getPassword());
body.put("name", request.getName());
if (request.getPreferences() != null) {
body.put("prefs", toJson(request.getPreferences()));
}
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH, null))
.post(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
User user = executeRequest(httpRequest, User.class);
log.info("User registered successfully: {}", user.getId());
return user;
} catch (AppwriteException e) {
log.error("User registration failed: {}", request.getEmail(), e);
throw e;
}
}
public AuthResponse login(LoginRequest request) throws AppwriteException {
log.info("User login attempt: {}", request.getEmail());
try {
Map<String, Object> body = new HashMap<>();
body.put("email", request.getEmail());
body.put("password", request.getPassword());
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/sessions", null))
.post(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
Session session = executeRequest(httpRequest, Session.class);
// Get user details
User user = getCurrentUser(session.getId());
AuthResponse response = AuthResponse.builder()
.user(user)
.session(session)
.expiresAt(Instant.parse(session.getExpire()))
.build();
log.info("User login successful: {}", user.getId());
return response;
} catch (AppwriteException e) {
log.error("User login failed: {}", request.getEmail(), e);
throw e;
}
}
public User getCurrentUser(String sessionId) throws AppwriteException {
try {
// Set session token for this request
AppwriteSessionContext.setCurrentToken(sessionId);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH, null))
.get()
.build();
User user = executeRequest(httpRequest, User.class);
return user;
} finally {
AppwriteSessionContext.clear();
}
}
public Session getSession(String sessionId) throws AppwriteException {
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/sessions/current", null))
.get()
.build();
return executeRequest(httpRequest, Session.class);
} finally {
AppwriteSessionContext.clear();
}
}
public void logout(String sessionId) throws AppwriteException {
log.info("Logging out session: {}", sessionId);
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/sessions/current", null))
.delete()
.build();
executeRequest(httpRequest, Void.class);
log.info("Session logged out successfully: {}", sessionId);
} finally {
AppwriteSessionContext.clear();
}
}
public void logoutAllSessions(String sessionId) throws AppwriteException {
log.info("Logging out all sessions for user");
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/sessions", null))
.delete()
.build();
executeRequest(httpRequest, Void.class);
log.info("All sessions logged out successfully");
} finally {
AppwriteSessionContext.clear();
}
}
public void updatePassword(String sessionId, String currentPassword, String newPassword) throws AppwriteException {
log.info("Updating user password");
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Map<String, Object> body = new HashMap<>();
body.put("password", newPassword);
body.put("oldPassword", currentPassword);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/password", null))
.patch(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
executeRequest(httpRequest, Void.class);
log.info("Password updated successfully");
} finally {
AppwriteSessionContext.clear();
}
}
public void updateEmail(String sessionId, String email, String password) throws AppwriteException {
log.info("Updating user email to: {}", email);
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Map<String, Object> body = new HashMap<>();
body.put("email", email);
body.put("password", password);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/email", null))
.patch(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
executeRequest(httpRequest, Void.class);
log.info("Email updated successfully");
} finally {
AppwriteSessionContext.clear();
}
}
public void updatePreferences(String sessionId, Map<String, Object> preferences) throws AppwriteException {
log.info("Updating user preferences");
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Map<String, Object> body = new HashMap<>();
body.put("prefs", preferences);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/prefs", null))
.patch(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
executeRequest(httpRequest, Void.class);
log.info("Preferences updated successfully");
} finally {
AppwriteSessionContext.clear();
}
}
public void sendPasswordReset(String email) throws AppwriteException {
log.info("Sending password reset for: {}", email);
Map<String, Object> body = new HashMap<>();
body.put("email", email);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/recovery", null))
.post(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
executeRequest(httpRequest, Void.class);
log.info("Password reset email sent successfully");
}
public void confirmPasswordReset(String userId, String secret, String newPassword) throws AppwriteException {
log.info("Confirming password reset for user: {}", userId);
Map<String, Object> body = new HashMap<>();
body.put("userId", userId);
body.put("secret", secret);
body.put("password", newPassword);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/recovery", null))
.put(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
executeRequest(httpRequest, Void.class);
log.info("Password reset confirmed successfully");
}
public List<Session> getSessions(String sessionId) throws AppwriteException {
try {
AppwriteSessionContext.setCurrentToken(sessionId);
Request httpRequest = buildRequest()
.url(buildUrl(AUTH_PATH + "/sessions", null))
.get()
.build();
AppwriteListResponse<Session> response = executeRequest(httpRequest, 
objectMapper.getTypeFactory().constructParametricType(AppwriteListResponse.class, Session.class));
return response.getSessions();
} finally {
AppwriteSessionContext.clear();
}
}
}
@Data
class AppwriteListResponse<T> {
private int total;
private List<T> sessions;
private List<T> users;
private List<T> documents;
private List<T> files;
private List<T> teams;
public List<T> getResults() {
if (sessions != null) return sessions;
if (users != null) return users;
if (documents != null) return documents;
if (files != null) return files;
if (teams != null) return teams;
return Collections.emptyList();
}
}

Database Service

1. Database Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Document {
private String id;
private String collectionId;
private String databaseId;
private String createdAt;
private String updatedAt;
private String[] permissions;
private Map<String, Object> data;
public <T> T getData(Class<T> type, ObjectMapper mapper) {
if (data == null) {
return null;
}
return mapper.convertValue(data, type);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Query {
private String field;
private Operator operator;
private Object value;
public enum Operator {
EQUAL("equal"),
NOT_EQUAL("notEqual"),
LESS_THAN("lessThan"),
LESS_THAN_EQUAL("lessThanEqual"),
GREATER_THAN("greaterThan"),
GREATER_THAN_EQUAL("greaterThanEqual"),
CONTAINS("contains"),
SEARCH("search"),
IS_NULL("isNull"),
IS_NOT_NULL("isNotNull");
private final String value;
Operator(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
public String toQueryString() {
return String.format("%s.%s(%s)", field, operator.getValue(), 
value instanceof String ? "\"" + value + "\"" : value);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DatabaseListOptions {
private List<String> queries;
private Integer limit;
private Integer offset;
private String cursor;
private String cursorDirection;
private String orderField;
private String orderType; // "ASC" or "DESC"
public Map<String, String> toQueryParams() {
Map<String, String> params = new HashMap<>();
if (queries != null && !queries.isEmpty()) {
params.put("queries", String.join(",", queries));
}
if (limit != null) {
params.put("limit", limit.toString());
}
if (offset != null) {
params.put("offset", offset.toString());
}
if (cursor != null) {
params.put("cursor", cursor);
}
if (cursorDirection != null) {
params.put("cursorDirection", cursorDirection);
}
if (orderField != null) {
params.put("orderField", orderField);
}
if (orderType != null) {
params.put("orderType", orderType);
}
return params;
}
}
2. Database Service Implementation
@Service
@Slf4j
public class AppwriteDatabaseService extends AppwriteService {
private static final String DATABASE_PATH = "/databases/%s/collections/%s/documents";
public AppwriteDatabaseService(OkHttpClient httpClient,
ObjectMapper objectMapper,
AppwriteProperties properties) {
super(httpClient, objectMapper, properties);
}
private String buildCollectionPath(String collectionId) {
return String.format(DATABASE_PATH, properties.getDatabaseId(), collectionId);
}
public <T> Document createDocument(String collectionId, T data, String[] permissions) throws AppwriteException {
log.info("Creating document in collection: {}", collectionId);
try {
Map<String, Object> body = new HashMap<>();
body.put("documentId", "unique()");
body.put("data", data);
if (permissions != null) {
body.put("permissions", permissions);
}
Request httpRequest = buildRequest()
.url(buildUrl(buildCollectionPath(collectionId), null))
.post(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
Document document = executeRequest(httpRequest, Document.class);
log.info("Document created successfully: {}", document.getId());
return document;
} catch (AppwriteException e) {
log.error("Document creation failed in collection: {}", collectionId, e);
throw e;
}
}
public Document getDocument(String collectionId, String documentId) throws AppwriteException {
log.debug("Getting document: {} from collection: {}", documentId, collectionId);
String path = buildCollectionPath(collectionId) + "/" + documentId;
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.get()
.build();
return executeRequest(httpRequest, Document.class);
}
public <T> Document updateDocument(String collectionId, String documentId, T data, String[] permissions) throws AppwriteException {
log.info("Updating document: {} in collection: {}", documentId, collectionId);
try {
Map<String, Object> body = new HashMap<>();
body.put("data", data);
if (permissions != null) {
body.put("permissions", permissions);
}
String path = buildCollectionPath(collectionId) + "/" + documentId;
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.patch(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
Document document = executeRequest(httpRequest, Document.class);
log.info("Document updated successfully: {}", document.getId());
return document;
} catch (AppwriteException e) {
log.error("Document update failed: {} in collection: {}", documentId, collectionId, e);
throw e;
}
}
public void deleteDocument(String collectionId, String documentId) throws AppwriteException {
log.info("Deleting document: {} from collection: {}", documentId, collectionId);
String path = buildCollectionPath(collectionId) + "/" + documentId;
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.delete()
.build();
executeRequest(httpRequest, Void.class);
log.info("Document deleted successfully: {}", documentId);
}
public List<Document> listDocuments(String collectionId, DatabaseListOptions options) throws AppwriteException {
log.debug("Listing documents from collection: {}", collectionId);
Map<String, String> queryParams = options != null ? options.toQueryParams() : null;
Request httpRequest = buildRequest()
.url(buildUrl(buildCollectionPath(collectionId), queryParams))
.get()
.build();
AppwriteListResponse<Document> response = executeRequest(httpRequest,
objectMapper.getTypeFactory().constructParametricType(AppwriteListResponse.class, Document.class));
return response.getDocuments();
}
public <T> List<T> listDocuments(String collectionId, DatabaseListOptions options, Class<T> dataType) throws AppwriteException {
List<Document> documents = listDocuments(collectionId, options);
return documents.stream()
.map(doc -> doc.getData(dataType, objectMapper))
.collect(Collectors.toList());
}
public List<Document> queryDocuments(String collectionId, List<Query> queries, DatabaseListOptions options) throws AppwriteException {
DatabaseListOptions queryOptions = options != null ? options : new DatabaseListOptions();
List<String> queryStrings = queries.stream()
.map(Query::toQueryString)
.collect(Collectors.toList());
queryOptions.setQueries(queryStrings);
return listDocuments(collectionId, queryOptions);
}
public <T> List<T> queryDocuments(String collectionId, List<Query> queries, DatabaseListOptions options, Class<T> dataType) throws AppwriteException {
List<Document> documents = queryDocuments(collectionId, queries, options);
return documents.stream()
.map(doc -> doc.getData(dataType, objectMapper))
.collect(Collectors.toList());
}
// Convenience methods for common queries
public <T> List<T> findDocumentsByField(String collectionId, String field, Object value, Class<T> dataType) throws AppwriteException {
Query query = Query.builder()
.field(field)
.operator(Query.Operator.EQUAL)
.value(value)
.build();
return queryDocuments(collectionId, List.of(query), null, dataType);
}
public <T> T findDocumentById(String collectionId, String documentId, Class<T> dataType) throws AppwriteException {
Document document = getDocument(collectionId, documentId);
return document != null ? document.getData(dataType, objectMapper) : null;
}
public boolean documentExists(String collectionId, String documentId) {
try {
getDocument(collectionId, documentId);
return true;
} catch (AppwriteException e) {
if (e.isClientError() && e.getHttpStatus() == 404) {
return false;
}
throw e;
}
}
}

Storage Service

1. Storage Models
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class File {
private String id;
private String bucketId;
private String name;
private long size;
private String mimeType;
private String signature;
private String[] permissions;
private String uploadedBy;
private String uploadedAt;
@JsonIgnore
public String getExtension() {
if (name == null) return "";
int lastDot = name.lastIndexOf('.');
return lastDot > 0 ? name.substring(lastDot + 1) : "";
}
@JsonIgnore
public boolean isImage() {
return mimeType != null && mimeType.startsWith("image/");
}
@JsonIgnore
public boolean isDocument() {
return mimeType != null && (
mimeType.startsWith("text/") ||
mimeType.equals("application/pdf") ||
mimeType.startsWith("application/msword") ||
mimeType.startsWith("application/vnd.openxmlformats")
);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileUpload {
private String filename;
private byte[] content;
private String mimeType;
private String[] permissions;
public static FileUpload fromPath(String filePath, String[] permissions) throws IOException {
Path path = Paths.get(filePath);
String filename = path.getFileName().toString();
byte[] content = Files.readAllBytes(path);
String mimeType = Files.probeContentType(path);
return FileUpload.builder()
.filename(filename)
.content(content)
.mimeType(mimeType)
.permissions(permissions)
.build();
}
}
2. Storage Service Implementation
@Service
@Slf4j
public class AppwriteStorageService extends AppwriteService {
private static final String STORAGE_PATH = "/storage/buckets/%s/files";
public AppwriteStorageService(OkHttpClient httpClient,
ObjectMapper objectMapper,
AppwriteProperties properties) {
super(httpClient, objectMapper, properties);
}
private String buildBucketPath(String bucketId) {
return String.format(STORAGE_PATH, bucketId);
}
public File uploadFile(String bucketId, FileUpload fileUpload) throws AppwriteException {
log.info("Uploading file: {} to bucket: {}", fileUpload.getFilename(), bucketId);
try {
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("fileId", "unique()")
.addFormDataPart("file", fileUpload.getFilename(),
RequestBody.create(fileUpload.getContent(), 
MediaType.parse(fileUpload.getMimeType())));
if (fileUpload.getPermissions() != null) {
bodyBuilder.addFormDataPart("permissions", 
String.join(",", fileUpload.getPermissions()));
}
Request httpRequest = buildRequest()
.url(buildUrl(buildBucketPath(bucketId), null))
.post(bodyBuilder.build())
.build();
File file = executeRequest(httpRequest, File.class);
log.info("File uploaded successfully: {}", file.getId());
return file;
} catch (AppwriteException e) {
log.error("File upload failed: {} to bucket: {}", fileUpload.getFilename(), bucketId, e);
throw e;
}
}
public File getFile(String bucketId, String fileId) throws AppwriteException {
log.debug("Getting file info: {} from bucket: {}", fileId, bucketId);
String path = buildBucketPath(bucketId) + "/" + fileId;
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.get()
.build();
return executeRequest(httpRequest, File.class);
}
public byte[] downloadFile(String bucketId, String fileId) throws AppwriteException {
log.debug("Downloading file: {} from bucket: {}", fileId, bucketId);
try {
String path = buildBucketPath(bucketId) + "/" + fileId + "/download";
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.get()
.build();
Response response = httpClient.newCall(httpRequest).execute();
if (!response.isSuccessful()) {
handleErrorResponse(response, response.body().string());
}
return response.body().bytes();
} catch (IOException e) {
throw new AppwriteException("File download failed: " + e.getMessage(), e);
}
}
public String getFilePreview(String bucketId, String fileId, Integer width, Integer height, String gravity, 
Integer quality, Integer borderWidth, String borderColor, Integer borderRadius,
Integer opacity, Integer rotation, String background, String output) throws AppwriteException {
log.debug("Getting file preview: {} from bucket: {}", fileId, bucketId);
Map<String, String> queryParams = new HashMap<>();
if (width != null) queryParams.put("width", width.toString());
if (height != null) queryParams.put("height", height.toString());
if (gravity != null) queryParams.put("gravity", gravity);
if (quality != null) queryParams.put("quality", quality.toString());
if (borderWidth != null) queryParams.put("borderWidth", borderWidth.toString());
if (borderColor != null) queryParams.put("borderColor", borderColor);
if (borderRadius != null) queryParams.put("borderRadius", borderRadius.toString());
if (opacity != null) queryParams.put("opacity", opacity.toString());
if (rotation != null) queryParams.put("rotation", rotation.toString());
if (background != null) queryParams.put("background", background);
if (output != null) queryParams.put("output", output);
String path = buildBucketPath(bucketId) + "/" + fileId + "/preview";
Request httpRequest = buildRequest()
.url(buildUrl(path, queryParams))
.get()
.build();
// This returns a URL to the preview
Response response;
try {
response = httpClient.newCall(httpRequest).execute();
return response.request().url().toString();
} catch (IOException e) {
throw new AppwriteException("File preview failed: " + e.getMessage(), e);
}
}
public void deleteFile(String bucketId, String fileId) throws AppwriteException {
log.info("Deleting file: {} from bucket: {}", fileId, bucketId);
String path = buildBucketPath(bucketId) + "/" + fileId;
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.delete()
.build();
executeRequest(httpRequest, Void.class);
log.info("File deleted successfully: {}", fileId);
}
public List<File> listFiles(String bucketId, DatabaseListOptions options) throws AppwriteException {
log.debug("Listing files from bucket: {}", bucketId);
Map<String, String> queryParams = options != null ? options.toQueryParams() : null;
Request httpRequest = buildRequest()
.url(buildUrl(buildBucketPath(bucketId), queryParams))
.get()
.build();
AppwriteListResponse<File> response = executeRequest(httpRequest,
objectMapper.getTypeFactory().constructParametricType(AppwriteListResponse.class, File.class));
return response.getFiles();
}
public File updateFilePermissions(String bucketId, String fileId, String[] permissions) throws AppwriteException {
log.info("Updating permissions for file: {} in bucket: {}", fileId, bucketId);
Map<String, Object> body = new HashMap<>();
body.put("permissions", permissions);
String path = buildBucketPath(bucketId) + "/" + fileId;
Request httpRequest = buildRequest()
.url(buildUrl(path, null))
.put(RequestBody.create(toJson(body), MediaType.parse("application/json")))
.build();
File file = executeRequest(httpRequest, File.class);
log.info("File permissions updated successfully: {}", fileId);
return file;
}
public long getBucketUsage(String bucketId) throws AppwriteException {
List<File> files = listFiles(bucketId, null);
return files.stream().mapToLong(File::getSize).sum();
}
}

Business Service Examples

1. User Management Service
@Service
@Slf4j
public class UserManagementService {
private final AppwriteDatabaseService databaseService;
private final AppwriteAuthService authService;
private final AppwriteProperties properties;
private final ObjectMapper objectMapper;
public UserManagementService(AppwriteDatabaseService databaseService,
AppwriteAuthService authService,
AppwriteProperties properties,
ObjectMapper objectMapper) {
this.databaseService = databaseService;
this.authService = authService;
this.properties = properties;
this.objectMapper = objectMapper;
}
public UserProfile createUserProfile(String userId, UserProfile profile) throws AppwriteException {
log.info("Creating user profile for: {}", userId);
// Add user ID to profile
profile.setUserId(userId);
profile.setCreatedAt(Instant.now());
profile.setUpdatedAt(Instant.now());
String[] permissions = new String[]{
"read(\"user:" + userId + "\")",
"write(\"user:" + userId + "\")"
};
Document document = databaseService.createDocument(
properties.getUsers().getCollectionId(), 
profile, 
permissions
);
return document.getData(UserProfile.class, objectMapper);
}
public UserProfile getUserProfile(String userId) throws AppwriteException {
log.debug("Getting user profile for: {}", userId);
List<UserProfile> profiles = databaseService.findDocumentsByField(
properties.getUsers().getCollectionId(),
"userId",
userId,
UserProfile.class
);
return profiles.isEmpty() ? null : profiles.get(0);
}
public UserProfile updateUserProfile(String userId, UserProfile updates) throws AppwriteException {
log.info("Updating user profile for: {}", userId);
UserProfile existingProfile = getUserProfile(userId);
if (existingProfile == null) {
throw new AppwriteException("User profile not found", "profile_not_found", 404, "client");
}
// Update fields
if (updates.getFirstName() != null) {
existingProfile.setFirstName(updates.getFirstName());
}
if (updates.getLastName() != null) {
existingProfile.setLastName(updates.getLastName());
}
if (updates.getAvatar() != null) {
existingProfile.setAvatar(updates.getAvatar());
}
if (updates.getPreferences() != null) {
existingProfile.setPreferences(updates.getPreferences());
}
existingProfile.setUpdatedAt(Instant.now());
// Find the document ID
List<Document> documents = databaseService.queryDocuments(
properties.getUsers().getCollectionId(),
List.of(Query.builder()
.field("userId")
.operator(Query.Operator.EQUAL)
.value(userId)
.build()),
null
);
if (documents.isEmpty()) {
throw new AppwriteException("User profile document not found", "document_not_found", 404, "client");
}
String documentId = documents.get(0).getId();
Document updatedDocument = databaseService.updateDocument(
properties.getUsers().getCollectionId(),
documentId,
existingProfile,
null
);
return updatedDocument.getData(UserProfile.class, objectMapper);
}
public void deleteUserProfile(String userId) throws AppwriteException {
log.info("Deleting user profile for: {}", userId);
List<Document> documents = databaseService.queryDocuments(
properties.getUsers().getCollectionId(),
List.of(Query.builder()
.field("userId")
.operator(Query.Operator.EQUAL)
.value(userId)
.build()),
null
);
if (!documents.isEmpty()) {
databaseService.deleteDocument(
properties.getUsers().getCollectionId(),
documents.get(0).getId()
);
log.info("User profile deleted for: {}", userId);
}
}
public List<UserProfile> searchUsers(String searchTerm, int limit, int offset) throws AppwriteException {
log.debug("Searching users with term: {}", searchTerm);
List<Query> queries = new ArrayList<>();
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
queries.add(Query.builder()
.field("firstName")
.operator(Query.Operator.SEARCH)
.value(searchTerm)
.build());
queries.add(Query.builder()
.field("lastName")
.operator(Query.Operator.SEARCH)
.value(searchTerm)
.build());
queries.add(Query.builder()
.field("email")
.operator(Query.Operator.SEARCH)
.value(searchTerm)
.build());
}
DatabaseListOptions options = DatabaseListOptions.builder()
.limit(limit)
.offset(offset)
.orderField("createdAt")
.orderType("DESC")
.build();
return databaseService.queryDocuments(
properties.getUsers().getCollectionId(),
queries,
options,
UserProfile.class
);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
class UserProfile {
private String userId;
private String firstName;
private String lastName;
private String avatar;
private String bio;
private String location;
private String website;
private Map<String, Object> preferences;
private Instant createdAt;
private Instant updatedAt;
}
2. Product Catalog Service
@Service
@Slf4j
public class ProductCatalogService {
private final AppwriteDatabaseService databaseService;
private final AppwriteStorageService storageService;
private final AppwriteProperties properties;
private final ObjectMapper objectMapper;
public ProductCatalogService(AppwriteDatabaseService databaseService,
AppwriteStorageService storageService,
AppwriteProperties properties,
ObjectMapper objectMapper) {
this.databaseService = databaseService;
this.storageService = storageService;
this.properties = properties;
this.objectMapper = objectMapper;
}
public Product createProduct(Product product, FileUpload imageUpload) throws AppwriteException {
log.info("Creating product: {}", product.getName());
// Upload product image if provided
if (imageUpload != null) {
File imageFile = storageService.uploadFile(
properties.getStorage().getBucketId(),
imageUpload
);
product.setImageId(imageFile.getId());
product.setImageUrl("/files/" + imageFile.getId());
}
product.setId(null); // Let Appwrite generate the ID
product.setCreatedAt(Instant.now());
product.setUpdatedAt(Instant.now());
// Set permissions - allow public read, but only admin write
String[] permissions = new String[]{
"read(\"any\")",
"write(\"role:admin\")"
};
Document document = databaseService.createDocument(
properties.getProducts().getCollectionId(),
product,
permissions
);
Product createdProduct = document.getData(Product.class, objectMapper);
createdProduct.setId(document.getId());
log.info("Product created successfully: {}", createdProduct.getId());
return createdProduct;
}
public Product getProduct(String productId) throws AppwriteException {
return databaseService.findDocumentById(
properties.getProducts().getCollectionId(),
productId,
Product.class
);
}
public List<Product> listProducts(String category, Double minPrice, Double maxPrice, 
Boolean inStock, int limit, int offset) throws AppwriteException {
List<Query> queries = new ArrayList<>();
if (category != null) {
queries.add(Query.builder()
.field("category")
.operator(Query.Operator.EQUAL)
.value(category)
.build());
}
if (minPrice != null) {
queries.add(Query.builder()
.field("price")
.operator(Query.Operator.GREATER_THAN_EQUAL)
.value(minPrice)
.build());
}
if (maxPrice != null) {
queries.add(Query.builder()
.field("price")
.operator(Query.Operator.LESS_THAN_EQUAL)
.value(maxPrice)
.build());
}
if (inStock != null) {
queries.add(Query.builder()
.field("stock")
.operator(inStock ? Query.Operator.GREATER_THAN : Query.Operator.LESS_THAN_EQUAL)
.value(0)
.build());
}
DatabaseListOptions options = DatabaseListOptions.builder()
.limit(limit)
.offset(offset)
.orderField("createdAt")
.orderType("DESC")
.build();
return databaseService.queryDocuments(
properties.getProducts().getCollectionId(),
queries,
options,
Product.class
);
}
public Product updateProduct(String productId, Product updates) throws AppwriteException {
log.info("Updating product: {}", productId);
updates.setUpdatedAt(Instant.now());
Document document = databaseService.updateDocument(
properties.getProducts().getCollectionId(),
productId,
updates,
null
);
Product updatedProduct = document.getData(Product.class, objectMapper);
updatedProduct.setId(document.getId());
log.info("Product updated successfully: {}", productId);
return updatedProduct;
}
public void deleteProduct(String productId) throws AppwriteException {
log.info("Deleting product: {}", productId);
// Get product to check for associated image
Product product = getProduct(productId);
if (product != null && product.getImageId() != null) {
try {
storageService.deleteFile(properties.getStorage().getBucketId(), product.getImageId());
} catch (AppwriteException e) {
log.warn("Failed to delete product image: {}", product.getImageId(), e);
}
}
databaseService.deleteDocument(properties.getProducts().getCollectionId(), productId);
log.info("Product deleted successfully: {}", productId);
}
public List<String> getProductCategories() throws AppwriteException {
// This would typically use a distinct query or maintain a separate categories collection
List<Product> products = databaseService.listDocuments(
properties.getProducts().getCollectionId(),
null,
Product.class
);
return products.stream()
.map(Product::getCategory)
.filter(Objects::nonNull)
.distinct()
.sorted()
.collect(Collectors.toList());
}
public List<Product> searchProducts(String searchTerm) throws AppwriteException {
List<Query> queries = new ArrayList<>();
if (searchTerm != null && !searchTerm.trim().isEmpty()) {
queries.add(Query.builder()
.field("name")
.operator(Query.Operator.SEARCH)
.value(searchTerm)
.build());
queries.add(Query.builder()
.field("description")
.operator(Query.Operator.SEARCH)
.value(searchTerm)
.build());
queries.add(Query.builder()
.field("tags")
.operator(Query.Operator.SEARCH)
.value(searchTerm)
.build());
}
DatabaseListOptions options = DatabaseListOptions.builder()
.limit(50)
.orderField("createdAt")
.orderType("DESC")
.build();
return databaseService.queryDocuments(
properties.getProducts().getCollectionId(),
queries,
options,
Product.class
);
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
class Product {
private String id;
private String name;
private String description;
private String category;
private Double price;
private String currency;
private Integer stock;
private List<String> tags;
private Map<String, Object> attributes;
private String imageId;
private String imageUrl;
private Boolean active;
private Instant createdAt;
private Instant updatedAt;
}

REST Controllers

1. Authentication Controller
@RestController
@RequestMapping("/api/auth")
@Validated
@Slf4j
public class AuthController {
private final AppwriteAuthService authService;
private final UserManagementService userManagementService;
public AuthController(AppwriteAuthService authService,
UserManagementService userManagementService) {
this.authService = authService;
this.userManagementService = userManagementService;
}
@PostMapping("/register")
public ResponseEntity<ApiResponse<User>> register(@Valid @RequestBody RegisterRequest request) {
try {
User user = authService.register(request);
// Create user profile
UserProfile profile = UserProfile.builder()
.firstName(request.getName())
.email(request.getEmail())
.preferences(request.getPreferences())
.build();
userManagementService.createUserProfile(user.getId(), profile);
return ResponseEntity.ok(ApiResponse.success(user));
} catch (AppwriteException e) {
log.error("Registration failed for: {}", request.getEmail(), e);
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@PostMapping("/login")
public ResponseEntity<ApiResponse<AuthResponse>> login(@Valid @RequestBody LoginRequest request) {
try {
AuthResponse authResponse = authService.login(request);
return ResponseEntity.ok(ApiResponse.success(authResponse));
} catch (AppwriteException e) {
log.error("Login failed for: {}", request.getEmail(), e);
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@GetMapping("/me")
public ResponseEntity<ApiResponse<User>> getCurrentUser(@RequestHeader("X-Session-Token") String sessionToken) {
try {
User user = authService.getCurrentUser(sessionToken);
return ResponseEntity.ok(ApiResponse.success(user));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@PostMapping("/logout")
public ResponseEntity<ApiResponse<Void>> logout(@RequestHeader("X-Session-Token") String sessionToken) {
try {
authService.logout(sessionToken);
return ResponseEntity.ok(ApiResponse.success(null, "Logged out successfully"));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@PostMapping("/password/reset")
public ResponseEntity<ApiResponse<Void>> sendPasswordReset(@RequestParam String email) {
try {
authService.sendPasswordReset(email);
return ResponseEntity.ok(ApiResponse.success(null, "Password reset email sent"));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class ApiResponse<T> {
private boolean success;
private String message;
private T data;
private String errorCode;
private Instant timestamp;
public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.data(data)
.timestamp(Instant.now())
.build();
}
public static <T> ApiResponse<T> success(T data, String message) {
return ApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.timestamp(Instant.now())
.build();
}
public static <T> ApiResponse<T> error(String message, String errorCode) {
return ApiResponse.<T>builder()
.success(false)
.message(message)
.errorCode(errorCode)
.timestamp(Instant.now())
.build();
}
}
2. Products Controller
@RestController
@RequestMapping("/api/products")
@Slf4j
public class ProductsController {
private final ProductCatalogService productCatalogService;
public ProductsController(ProductCatalogService productCatalogService) {
this.productCatalogService = productCatalogService;
}
@GetMapping
public ResponseEntity<ApiResponse<List<Product>>> getProducts(
@RequestParam(required = false) String category,
@RequestParam(required = false) Double minPrice,
@RequestParam(required = false) Double maxPrice,
@RequestParam(required = false) Boolean inStock,
@RequestParam(defaultValue = "20") int limit,
@RequestParam(defaultValue = "0") int offset) {
try {
List<Product> products = productCatalogService.listProducts(
category, minPrice, maxPrice, inStock, limit, offset);
return ResponseEntity.ok(ApiResponse.success(products));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@GetMapping("/{productId}")
public ResponseEntity<ApiResponse<Product>> getProduct(@PathVariable String productId) {
try {
Product product = productCatalogService.getProduct(productId);
if (product == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error("Product not found", "product_not_found"));
}
return ResponseEntity.ok(ApiResponse.success(product));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@GetMapping("/search")
public ResponseEntity<ApiResponse<List<Product>>> searchProducts(@RequestParam String q) {
try {
List<Product> products = productCatalogService.searchProducts(q);
return ResponseEntity.ok(ApiResponse.success(products));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@GetMapping("/categories")
public ResponseEntity<ApiResponse<List<String>>> getCategories() {
try {
List<String> categories = productCatalogService.getProductCategories();
return ResponseEntity.ok(ApiResponse.success(categories));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@PostMapping
public ResponseEntity<ApiResponse<Product>> createProduct(
@Valid @RequestBody Product product,
@RequestParam(value = "image", required = false) MultipartFile imageFile) {
try {
FileUpload imageUpload = null;
if (imageFile != null && !imageFile.isEmpty()) {
imageUpload = FileUpload.builder()
.filename(imageFile.getOriginalFilename())
.content(imageFile.getBytes())
.mimeType(imageFile.getContentType())
.build();
}
Product createdProduct = productCatalogService.createProduct(product, imageUpload);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(createdProduct, "Product created successfully"));
} catch (AppwriteException | IOException e) {
log.error("Product creation failed", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error("Product creation failed", "creation_failed"));
}
}
@PutMapping("/{productId}")
public ResponseEntity<ApiResponse<Product>> updateProduct(
@PathVariable String productId,
@Valid @RequestBody Product product) {
try {
Product updatedProduct = productCatalogService.updateProduct(productId, product);
return ResponseEntity.ok(ApiResponse.success(updatedProduct, "Product updated successfully"));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
@DeleteMapping("/{productId}")
public ResponseEntity<ApiResponse<Void>> deleteProduct(@PathVariable String productId) {
try {
productCatalogService.deleteProduct(productId);
return ResponseEntity.ok(ApiResponse.success(null, "Product deleted successfully"));
} catch (AppwriteException e) {
return ResponseEntity.status(e.getHttpStatus())
.body(ApiResponse.error(e.getMessage(), e.getErrorCode()));
}
}
}

Error Handling

@ControllerAdvice
@Slf4j
public class AppwriteExceptionHandler {
@ExceptionHandler(AppwriteException.class)
public ResponseEntity<ApiResponse<Object>> handleAppwriteException(AppwriteException e) {
log.error("Appwrite exception occurred", e);
ApiResponse<Object> response = ApiResponse.error(e.getMessage(), e.getErrorCode());
return ResponseEntity.status(e.getHttpStatus()).body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception e) {
log.error("Unexpected error occurred", e);
ApiResponse<Object> response = ApiResponse.error(
"An unexpected error occurred", "internal_error");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Object>> handleValidationException(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());
ApiResponse<Object> response = ApiResponse.error(
"Validation failed: " + String.join(", ", errors), "validation_failed");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
}

Testing

1. Unit Tests
@ExtendWith(MockitoExtension.class)
class AppwriteAuthServiceTest {
@Mock
private OkHttpClient httpClient;
@Mock
private ObjectMapper objectMapper;
@Mock
private AppwriteProperties properties;
@InjectMocks
private AppwriteAuthService authService;
@Test
void testLogin_Success() throws Exception {
// Setup
LoginRequest request = new LoginRequest("[email protected]", "password");
Session session = new Session();
session.setId("session123");
session.setUserId("user123");
when(properties.getEndpoint()).thenReturn("http://localhost:8081/v1");
// Mock HTTP response and object mapping
// Execute & Verify
// Test implementation would mock the HTTP calls and verify behavior
}
}
@SpringBootTest
class ProductCatalogServiceIntegrationTest {
@Autowired
private ProductCatalogService productCatalogService;
@Test
void testCreateProduct() {
Product product = Product.builder()
.name("Test Product")
.description("Test Description")
.price(99.99)
.category("Electronics")
.build();
// This would test against a real Appwrite instance in integration tests
// Product createdProduct = productCatalogService.createProduct(product, null);
// assertNotNull(createdProduct);
// assertNotNull(createdProduct.getId());
}
}
2. Test Configuration
@TestConfiguration
public class TestAppwriteConfig {
@Bean
@Primary
public AppwriteProperties testAppwriteProperties() {
AppwriteProperties properties = new AppwriteProperties();
properties.setEndpoint("http://localhost:8081/v1");
properties.setProjectId("test-project");
properties.setApiKey("test-api-key");
properties.setDatabaseId("test-database");
AppwriteProperties.ServiceConfig usersConfig = new AppwriteProperties.ServiceConfig();
usersConfig.setCollectionId("users");
properties.setUsers(usersConfig);
AppwriteProperties.ServiceConfig productsConfig = new AppwriteProperties.ServiceConfig();
productsConfig.setCollectionId("products");
properties.setProducts(productsConfig);
return properties;
}
}

Best Practices

  1. Security:
  • Use environment variables for sensitive configuration
  • Implement proper error handling without information leakage
  • Validate all inputs
  • Use HTTPS in production
  1. Performance:
  • Implement connection pooling
  • Use appropriate timeouts
  • Implement retry mechanisms for transient failures
  • Cache frequently accessed data
  1. Maintainability:
  • Use structured logging
  • Implement comprehensive monitoring
  • Follow consistent error handling patterns
  • Use dependency injection
  1. Scalability:
  • Design for horizontal scaling
  • Use asynchronous processing where appropriate
  • Implement proper resource management
// Example of secure configuration validation
@Component
public class AppwriteConfigValidator {
public void validateConfiguration(AppwriteProperties properties) {
if (properties.getEndpoint() == null || properties.getEndpoint().trim().isEmpty()) {
throw new IllegalStateException("Appwrite endpoint is required");
}
if (properties.getProjectId() == null || properties.getProjectId().trim().isEmpty()) {
throw new IllegalStateException("Appwrite project ID is required");
}
if (properties.getApiKey() == null || properties.getApiKey().trim().isEmpty()) {
throw new IllegalStateException("Appwrite API key is required");
}
}
}

Conclusion

This comprehensive Appwrite Java integration provides:

  • Complete REST API client for all Appwrite services
  • Type-safe data models for requests and responses
  • Comprehensive error handling with proper exception hierarchy
  • Security best practices including authentication and authorization
  • Performance optimizations with connection pooling and retry mechanisms
  • Production-ready configuration and monitoring

The implementation follows Java and Spring Boot best practices while providing a robust foundation for building applications on top of Appwrite's backend services. It can be easily extended with additional Appwrite features and integrated into existing Spring Boot applications.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper