Create, read, update and delete Parse objects
Save, query, update and delete objects in your Back4app database, with the same four operations in every SDK, REST and GraphQL.
On Back4app, every row is a Parse object stored in a class. You create one by setting fields on a Parse.Object and calling save(), read it with a Parse.Query, update it by changing fields and saving again, and remove it with destroy(). The same four operations exist in every SDK and over REST and GraphQL; only the syntax changes.
This guide uses a class called Todo with two fields, title (String) and done (Boolean). You do not need to create the class first: Back4app creates the class and its columns the first time you save an object, unless you have disabled client class creation in App Settings → Security.
What you need
Section titled “What you need”A Back4app app and its keys, from Dashboard → App Settings → Security & Keys. Every SDK needs the Application ID plus one more key, and which one depends on the SDK:
| SDK | Second key | Sent as |
|---|---|---|
| JavaScript, React, React Native | JavaScript Key | Parse.initialize(appId, jsKey) |
| Flutter, Android, iOS | Client Key | clientKey / X-Parse-Client-Key |
| REST | REST API Key | X-Parse-REST-API-Key |
| GraphQL | Client Key | X-Parse-Client-Key |
Keep both values in environment variables. Never commit them, and never ship the Master Key in a client app: it bypasses every permission check.
The variable names differ per toolchain, because bundlers only expose variables carrying their own prefix:
| SDK | Variables |
|---|---|
| JavaScript (Node.js) | PARSE_APP_ID, PARSE_JS_KEY |
| React (Vite) | VITE_PARSE_APP_ID, VITE_PARSE_JS_KEY |
| React (Next.js) | NEXT_PUBLIC_PARSE_APP_ID, NEXT_PUBLIC_PARSE_JS_KEY |
| React Native (Expo) | EXPO_PUBLIC_PARSE_APP_ID, EXPO_PUBLIC_PARSE_JS_KEY |
| Flutter | PARSE_APP_ID, PARSE_CLIENT_KEY via --dart-define |
| Android | PARSE_APP_ID, PARSE_CLIENT_KEY via buildConfigField |
| iOS | PARSE_APP_ID, PARSE_CLIENT_KEY via an .xcconfig file |
| REST | PARSE_APP_ID, PARSE_REST_KEY |
| GraphQL | PARSE_APP_ID, PARSE_CLIENT_KEY |
Install the SDK if you have not done it yet:
npm install parse@^7 # Node.js 20 or laternpm install parse@^7npm install parse@^7 @react-native-async-storage/async-storagedependencies: parse_server_sdk_flutter: ^9.0.0dependencies { implementation("com.github.parse-community.Parse-SDK-Android:parse:4.3.0")}Add maven { url = uri("https://jitpack.io") } to your repositories.
Requires iOS 15 or later and Swift 5.9. Add the package in Xcode under File → Add Package Dependencies:
https://github.com/netreconlab/Parse-Swift Up to Next Major Version: 5.0.0Or in Package.swift:
.package(url: "https://github.com/netreconlab/Parse-Swift.git", from: "5.0.0")Nothing to install. Any HTTP client works.
Nothing to install. Any GraphQL client or plain HTTP works.
-
Connect to your app
Section titled “Connect to your app”Initialize the SDK once, when the app starts. The server URL is the same for every app on Back4app.
parse.js // Browser and bundlers: 'parse'. Node.js: 'parse/node.js'.import Parse from 'parse/node.js';Parse.initialize(process.env.PARSE_APP_ID, process.env.PARSE_JS_KEY);Parse.serverURL = 'https://parseapi.back4app.com';export default Parse;In Node.js the variables are
PARSE_APP_IDandPARSE_JS_KEY. Other toolchains need their own prefix, listed in the table above.src/lib/parse.js import Parse from 'parse';Parse.initialize(import.meta.env.VITE_PARSE_APP_ID, import.meta.env.VITE_PARSE_JS_KEY);Parse.serverURL = 'https://parseapi.back4app.com';export default Parse;Import this module once in
main.jsxso the SDK is configured before any component renders.This example assumes Vite. On Next.js, use
NEXT_PUBLIC_variables instead and import the module from a Client Component, because the Parse browser SDK does not run during server rendering:lib/parse.js (Next.js) 'use client';import Parse from 'parse';Parse.initialize(process.env.NEXT_PUBLIC_PARSE_APP_ID, process.env.NEXT_PUBLIC_PARSE_JS_KEY);Parse.serverURL = 'https://parseapi.back4app.com';export default Parse;src/lib/parse.js import Parse from 'parse/react-native.js';import AsyncStorage from '@react-native-async-storage/async-storage';Parse.setAsyncStorage(AsyncStorage);Parse.initialize(process.env.EXPO_PUBLIC_PARSE_APP_ID, process.env.EXPO_PUBLIC_PARSE_JS_KEY);Parse.serverURL = 'https://parseapi.back4app.com';export default Parse;lib/main.dart import 'package:parse_server_sdk_flutter/parse_server_sdk_flutter.dart';Future<void> main() async {WidgetsFlutterBinding.ensureInitialized();await Parse().initialize(const String.fromEnvironment('PARSE_APP_ID'),'https://parseapi.back4app.com',clientKey: const String.fromEnvironment('PARSE_CLIENT_KEY'),autoSendSessionId: true,);runApp(const MyApp());}String.fromEnvironmentreads a compile-time constant, so pass the values when you build:Terminal window flutter run --dart-define=PARSE_APP_ID=... --dart-define=PARSE_CLIENT_KEY=...App.kt class App : Application() {override fun onCreate() {super.onCreate()Parse.initialize(Parse.Configuration.Builder(this).applicationId(BuildConfig.PARSE_APP_ID).clientKey(BuildConfig.PARSE_CLIENT_KEY).server("https://parseapi.back4app.com").build())}}Register
AppinAndroidManifest.xmlwithandroid:name=".App".BuildConfigfields need to be declared and enabled inbuild.gradle.kts:build.gradle.kts val parseAppId: String = providers.gradleProperty("PARSE_APP_ID").getOrElse("")val parseClientKey: String = providers.gradleProperty("PARSE_CLIENT_KEY").getOrElse("")android {buildFeatures { buildConfig = true } // required from AGP 8defaultConfig {buildConfigField("String", "PARSE_APP_ID", "\"" + parseAppId + "\"")buildConfigField("String", "PARSE_CLIENT_KEY", "\"" + parseClientKey + "\"")}}Put the two values in
gradle.propertiesoutside the repository, or pass them on the command line with-PPARSE_APP_ID=your-id -PPARSE_CLIENT_KEY=your-key.Put the keys in an
.xcconfigfile that is git-ignored, and surface them throughInfo.plistso the app can read them at runtime:Secrets.xcconfig (git-ignored) PARSE_APP_ID = your-application-idPARSE_CLIENT_KEY = your-client-keyAdd two
Info.plistentries with the values$(PARSE_APP_ID)and$(PARSE_CLIENT_KEY), then:MyApp.swift import ParseSwift@mainstruct MyApp: App {init() {let info = Bundle.main.infoDictionaryParseSwift.initialize(applicationId: info?["PARSE_APP_ID"] as? String ?? "",clientKey: info?["PARSE_CLIENT_KEY"] as? String ?? "",serverURL: URL(string: "https://parseapi.back4app.com")!)}var body: some Scene { WindowGroup { ContentView() } }}Every request carries two headers. Export the keys once in your shell:
Terminal window export PARSE_APP_ID="your-application-id"export PARSE_REST_KEY="your-rest-api-key"The endpoint is
https://parseapi.back4app.com/graphql. Every operation is aPOSTwhose body is a JSON object with aqueryfield:Terminal window export PARSE_APP_ID="your-application-id"export PARSE_CLIENT_KEY="your-client-key"curl -X POST https://parseapi.back4app.com/graphql \-H "X-Parse-Application-Id: $PARSE_APP_ID" \-H "X-Parse-Client-Key: $PARSE_CLIENT_KEY" \-H "Content-Type: application/json" \-d '{"query":"query { health }"}'Response { "data": { "health": true } }The snippets below show only the GraphQL document. Send each one in that same
queryfield. -
Create an object
Section titled “Create an object”Set the fields and save. The server returns the
objectIdandcreatedAtof the new row.const todo = new Parse.Object('Todo');todo.set('title', 'Buy milk');todo.set('done', false);const saved = await todo.save();console.log(saved.id, saved.createdAt);Output“xKue915KBG” 2026-09-30T14:02:11.318Z
src/AddTodo.jsx import { useState } from 'react';import Parse from './lib/parse';export function AddTodo({ onCreated }) {const [title, setTitle] = useState('');async function handleSubmit(e) {e.preventDefault();const todo = new Parse.Object('Todo');todo.set('title', title);todo.set('done', false);const saved = await todo.save();setTitle('');onCreated(saved);}return (<form onSubmit={handleSubmit}><input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="New todo" /><button type="submit">Add</button></form>);}import Parse from './lib/parse';export async function createTodo(title) {const todo = new Parse.Object('Todo');todo.set('title', title);todo.set('done', false);const saved = await todo.save();return saved.id; // "xKue915KBG"}final todo = ParseObject('Todo')..set('title', 'Buy milk')..set('done', false);final response = await todo.save();if (response.success) {print('${todo.objectId} ${todo.createdAt}');} else {print(response.error?.message);}val todo = ParseObject("Todo")todo.put("title", "Buy milk")todo.put("done", false)todo.saveInBackground { e ->if (e == null) Log.d("Todo", "Saved ${todo.objectId}")else Log.e("Todo", "Save failed: ${e.message}")}struct Todo: ParseObject {var objectId: String?var createdAt: Date?var updatedAt: Date?var ACL: ParseACL?var originalData: Data?var title: String?var done: Bool?}var todo = Todo()todo.title = "Buy milk"todo.done = falselet saved = try await todo.save()print(saved.objectId!, saved.createdAt!)Terminal window curl -X POST https://parseapi.back4app.com/classes/Todo \-H "X-Parse-Application-Id: $PARSE_APP_ID" \-H "X-Parse-REST-API-Key: $PARSE_REST_KEY" \-H "Content-Type: application/json" \-d '{"title": "Buy milk", "done": false}'Response (201 Created) { "objectId": "xKue915KBG", "createdAt": "2026-09-30T14:02:11.318Z" }mutation {createTodo(input: { fields: { title: "Buy milk", done: false } }) {todo { objectId createdAt }}}Response { "data": { "createTodo": { "todo": { "objectId": "xKue915KBG", "createdAt": "2026-09-30T14:02:11.318Z" } } } } -
Read objects
Section titled “Read objects”Fetch one object by its
objectId, or run a query with conditions, ordering and a limit. Queries return at most 100 objects by default; setlimitfor more, up to 1000. For anything beyond that, page through the results withskip, covered in Queries.find()returns an empty array when nothing matches, nevernull, so always check the length before reading the first element. If the array is exactly as long as yourlimit, there are probably more rows: callcount()for the total, or fetch the next page withskip.// One object by idconst todo = await new Parse.Query('Todo').get('xKue915KBG');console.log(todo.get('title')); // "Buy milk"// Open todos, newest firstconst query = new Parse.Query('Todo');query.equalTo('done', false);query.descending('createdAt');query.limit(10);const open = await query.find();open.forEach((t) => console.log(t.id, t.get('title')));src/TodoList.jsx import { useEffect, useState } from 'react';import Parse from './lib/parse';export function TodoList() {const [todos, setTodos] = useState([]);useEffect(() => {const query = new Parse.Query('Todo');query.equalTo('done', false);query.descending('createdAt');query.limit(10);query.find().then(setTodos);}, []);return (<ul>{todos.map((t) => <li key={t.id}>{t.get('title')}</li>)}</ul>);}export async function listOpenTodos() {const query = new Parse.Query('Todo');query.equalTo('done', false);query.descending('createdAt');query.limit(10);const results = await query.find();return results.map((t) => ({ id: t.id, title: t.get('title') }));}// One object by idfinal byId = await ParseObject('Todo').getObject('xKue915KBG');if (byId.success) print(byId.results?.first.get<String>('title'));// Open todos, newest firstfinal query = QueryBuilder<ParseObject>(ParseObject('Todo'))..whereEqualTo('done', false)..orderByDescending('createdAt')..setLimit(10);final response = await query.query();if (response.success && response.results != null) {for (final t in response.results!) {print('${t.objectId} ${t.get<String>('title')}');}}// One object by idParseQuery.getQuery<ParseObject>("Todo").getInBackground("xKue915KBG") { todo, e ->if (e == null) Log.d("Todo", todo.getString("title") ?: "")}// Open todos, newest firstval query = ParseQuery.getQuery<ParseObject>("Todo")query.whereEqualTo("done", false)query.orderByDescending("createdAt")query.limit = 10query.findInBackground { todos, e ->if (e == null) todos.forEach { Log.d("Todo", "${it.objectId} ${it.getString("title")}") }}// One object by idlet todo = try await Todo(objectId: "xKue915KBG").fetch()print(todo.title ?? "")// Open todos, newest firstlet open = try await Todo.query("done" == false).order([.descending("createdAt")]).limit(10).find()open.forEach { print($0.objectId ?? "", $0.title ?? "") }Terminal window # One object by idcurl https://parseapi.back4app.com/classes/Todo/xKue915KBG \-H "X-Parse-Application-Id: $PARSE_APP_ID" \-H "X-Parse-REST-API-Key: $PARSE_REST_KEY"# Open todos, newest firstcurl -G https://parseapi.back4app.com/classes/Todo \-H "X-Parse-Application-Id: $PARSE_APP_ID" \-H "X-Parse-REST-API-Key: $PARSE_REST_KEY" \--data-urlencode 'where={"done": false}' \--data-urlencode 'order=-createdAt' \--data-urlencode 'limit=10'Response { "results": [ { "objectId": "xKue915KBG", "title": "Buy milk", "done": false, "createdAt": "…", "updatedAt": "…" } ] }query {todo(id: "xKue915KBG") { title done }todos(where: { done: { equalTo: false } }, order: [createdAt_DESC], first: 10) {edges { node { objectId title } }}} -
Update an object
Section titled “Update an object”Fetch the object, change the fields you need, and save. Only the changed fields are sent to the server, and
updatedAtis refreshed.const todo = await new Parse.Query('Todo').get('xKue915KBG');todo.set('done', true);await todo.save();console.log(todo.updatedAt);If you already know the
objectId, skip the fetch.createWithoutDatabuilds a local reference to an existing row without reading it first, so this is one request instead of two. Use it when you only need to write:const Todo = Parse.Object.extend('Todo');const todo = Todo.createWithoutData('xKue915KBG');todo.set('done', true);await todo.save();async function markDone(todo) {todo.set('done', true);await todo.save();setTodos((list) => list.filter((t) => t.id !== todo.id));}export async function markDone(id) {const todo = await new Parse.Query('Todo').get(id);todo.set('done', true);await todo.save();}final todo = ParseObject('Todo')..objectId = 'xKue915KBG';todo.set('done', true);final response = await todo.save();print(response.success ? 'updated' : response.error?.message);val todo = ParseObject.createWithoutData("Todo", "xKue915KBG")todo.put("done", true)todo.saveInBackground { e -> if (e == null) Log.d("Todo", "Updated") }var todo = try await Todo(objectId: "xKue915KBG").fetch()todo.done = truelet updated = try await todo.save()print(updated.updatedAt!)Terminal window curl -X PUT https://parseapi.back4app.com/classes/Todo/xKue915KBG \-H "X-Parse-Application-Id: $PARSE_APP_ID" \-H "X-Parse-REST-API-Key: $PARSE_REST_KEY" \-H "Content-Type: application/json" \-d '{"done": true}'Response { "updatedAt": "2026-09-30T14:05:40.102Z" }mutation {updateTodo(input: { id: "xKue915KBG", fields: { done: true } }) {todo { updatedAt }}} -
Delete an object
Section titled “Delete an object”Deleting is permanent. If other objects point to this one, the pointers keep the
objectIdbut resolve to nothing.const todo = await new Parse.Query('Todo').get('xKue915KBG');await todo.destroy();async function remove(todo) {await todo.destroy();setTodos((list) => list.filter((t) => t.id !== todo.id));}export async function deleteTodo(id) {const todo = await new Parse.Query('Todo').get(id);await todo.destroy();}final todo = ParseObject('Todo')..objectId = 'xKue915KBG';final response = await todo.delete();print(response.success ? 'deleted' : response.error?.message);val todo = ParseObject.createWithoutData("Todo", "xKue915KBG")todo.deleteInBackground { e -> if (e == null) Log.d("Todo", "Deleted") }let todo = Todo(objectId: "xKue915KBG")try await todo.delete()Terminal window curl -X DELETE https://parseapi.back4app.com/classes/Todo/xKue915KBG \-H "X-Parse-Application-Id: $PARSE_APP_ID" \-H "X-Parse-REST-API-Key: $PARSE_REST_KEY"Response {}mutation {deleteTodo(input: { id: "xKue915KBG" }) {todo { objectId }}}
Check the result in the dashboard
Section titled “Check the result in the dashboard”Open Dashboard → Database → Browser and select the Todo class. After step 2 you should see one row with title = "Buy milk" and done = false; after step 4 the same row shows done = true; after step 5 the class is empty.
Handle errors
Section titled “Handle errors”Every operation on this page can fail, and the failure carries a numeric code that tells you what to do. Branch on the code rather than on the message, because messages change between Parse Server versions.
The code is always in the same place; the human-readable text is not:
| Transport | Shape | Text field |
|---|---|---|
| SDKs (JS, Flutter, Android, Swift) | { code, message } |
message |
| REST | { "code": 101, "error": "Object not found." } |
error |
| GraphQL | { message, extensions: { code } } |
message |
A failure that never reached the server, such as no network or a wrong serverURL, is not a Parse.Error and has no code. Check for that first, otherwise you log Parse error undefined.
try { await todo.save();} catch (err) { if (!(err instanceof Parse.Error)) { // Network failure, wrong serverURL, or unset credentials. console.error('Could not reach Back4app:', err.message); throw err; } switch (err.code) { case Parse.Error.OBJECT_NOT_FOUND: // 101 console.error('That todo no longer exists.'); break; case Parse.Error.OPERATION_FORBIDDEN: // 119 console.error('Permission denied. Check the class CLP.'); break; case Parse.Error.DUPLICATE_VALUE: // 137 console.error('A unique index rejected that value.'); break; case Parse.Error.INVALID_SESSION_TOKEN: // 209 // Only meaningful when a user is logged in; a keys-only script has no session. if (Parse.User.current()) await Parse.User.logOut(); break; default: console.error(`Parse error ${err.code}: ${err.message}`); }}import { useState } from 'react';import Parse from './lib/parse';
export function AddTodo({ onCreated }) { const [title, setTitle] = useState(''); const [error, setError] = useState(null);
async function handleSubmit(e) { e.preventDefault(); const todo = new Parse.Object('Todo'); todo.set('title', title); todo.set('done', false); try { onCreated(await todo.save()); setTitle(''); setError(null); } catch (err) { if (!(err instanceof Parse.Error)) setError('Could not reach Back4app.'); else if (err.code === Parse.Error.OPERATION_FORBIDDEN) setError('You do not have permission to add todos.'); else setError(`Could not save (${err.code}).`); } }
return ( <form onSubmit={handleSubmit}> <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="New todo" /> <button type="submit">Add</button> {error && <p role="alert">{error}</p>} </form> );}export async function createTodo(title) { try { const todo = new Parse.Object('Todo'); todo.set('title', title); todo.set('done', false); return (await todo.save()).id; } catch (err) { if (!(err instanceof Parse.Error)) throw new Error('Could not reach Back4app.'); if (err.code === Parse.Error.INVALID_SESSION_TOKEN && Parse.User.current()) await Parse.User.logOut(); throw err; }}The Flutter SDK does not throw. Every call returns a ParseResponse; check success and read error:
final response = await todo.save();if (!response.success) { final code = response.error?.code; final message = response.error?.message; if (code == 119) { print('Permission denied. Check the class CLP.'); } else { print('Parse error $code: $message'); }}todo.saveInBackground { e -> when (e?.code) { null -> Log.d("Todo", "Saved ${todo.objectId}") ParseException.OBJECT_NOT_FOUND -> Log.e("Todo", "No longer exists") ParseException.OPERATION_FORBIDDEN -> Log.e("Todo", "Permission denied") else -> Log.e("Todo", "Parse error ${e.code}: ${e.message}") }}ParseError.code is an enum, not an integer, so match on the case name. Always keep a general catch: a network failure throws URLError, not ParseError.
do { let saved = try await todo.save() print(saved.objectId!)} catch let error as ParseError { switch error.code { case .objectNotFound: print("That todo no longer exists.") // 101 case .operationForbidden: print("Permission denied. Check the class CLP.") // 119 case .duplicateValue: print("A unique index rejected that value.") // 137 case .invalidSessionToken: print("Session expired. Sign in again.") // 209 default: print("Parse error \(error.code): \(error.message)") }} catch { print("Could not reach Back4app: \(error.localizedDescription)")}Errors come back with an HTTP status and a JSON body holding the same code:
{ "code": 101, "error": "Object not found." }Errors arrive in the errors array, with the Parse code under extensions:
{ "errors": [ { "message": "Object not found.", "extensions": { "code": 101 } } ] }Common errors
Section titled “Common errors”| Code | Meaning | Cause and fix |
|---|---|---|
| 101 | Object not found | The objectId does not exist, or the current user has no read access under the class’s ACL or CLP. |
| 119 | Permission denied | Client class creation is disabled, or the CLP blocks the operation for this user. Enable it in App Settings → Security or adjust the CLP in the Database Browser. |
| 137 | Duplicate value for a unique field | A unique index rejected the value. Query first, or catch the error and update instead. |
| 209 | Invalid session token | The stored session expired or was revoked. If a user is logged in, log them out and in again. A keys-only script has no session and should not call logOut(). |
A wrong Application ID or key does not produce a Parse code at all: the request is rejected with HTTP 403 before it reaches the application layer. REST needs the REST API Key; Flutter, Android and iOS need the Client Key.
Complete example
Section titled “Complete example”A minimal Node.js script that runs the four operations in order. Save it as crud.mjs and run node crud.mjs with PARSE_APP_ID and PARSE_JS_KEY set.
import Parse from 'parse/node.js';
Parse.initialize(process.env.PARSE_APP_ID, process.env.PARSE_JS_KEY);Parse.serverURL = 'https://parseapi.back4app.com';
try { // Create const todo = new Parse.Object('Todo'); todo.set('title', 'Buy milk'); todo.set('done', false); await todo.save(); console.log('created', todo.id);
// Read const open = await new Parse.Query('Todo') .equalTo('done', false) .descending('createdAt') .limit(10) .find(); console.log('open todos', open.map((t) => t.get('title')));
// Update the first result, if there is one const first = open[0]; if (!first) { console.log('nothing to update'); } else { first.set('done', true); await first.save(); console.log('updated', first.id, first.updatedAt);
// Delete await first.destroy(); console.log('deleted', first.id); }} catch (err) { if (err instanceof Parse.Error && err.code === Parse.Error.OPERATION_FORBIDDEN) { console.error('Permission denied. Check the class CLP in the dashboard.'); } else if (err instanceof Parse.Error) { console.error(`Parse error ${err.code}: ${err.message}`); } else { console.error('Could not reach Back4app:', err.message); } process.exitCode = 1;}created xKue915KBG open todos [ ‘Buy milk’ ] updated xKue915KBG 2026-09-30T14:05:40.102Z deleted xKue915KBG
Prompt for Claude Code, Cursor or Copilot
Add Parse CRUD to this project using the Back4app backend. Read https://www.back4app.com/docs/guides/database/create-read-update-delete.md first. Use the SDK that matches this project’s language. Read PARSE_APP_ID and the SDK key from environment variables; never hardcode them. Server URL: https://parseapi.back4app.com Create a
Todoclass withtitle(String) anddone(Boolean), then implement create, list open todos (newest first, limit 10), mark done, and delete. Handle Parse error codes 101, 119, 137 and 209 with clear messages.
Next steps
Section titled “Next steps”- Queries: filters, ordering, pagination and counting.
- Relations: pointers, one-to-many and many-to-many.
- Security: ACL and CLP: control who can read and write each object.
- REST API reference: Objects.