1---
2title: 'Tutorial: Create a module with a config plugin'
3sidebar_title: Create a module with a config plugin
4description: A tutorial on creating a native module with a config plugin using Expo modules API.
5---
6
7import { CodeBlocksTable } from '~/components/plugins/CodeBlocksTable';
8import { PlatformTag } from '~/ui/components/Tag';
9import { APIBox } from '~/components/plugins/APIBox';
10import { Terminal } from '~/ui/components/Snippet';
11
12Config plugins allow you to customize native Android and iOS projects when they are generated with `npx expo prebuild`. It is often useful to add properties in native config files, to copy assets to native projects, and for advanced configurations such as adding an app extension target. As an app developer, applying customizations not exposed in the default [Expo config](/workflow/configuration) can be helpful. As a library author, it allows you to configure native projects for the developers using your library automatically.
13
14This guide will walk you through creating a new config plugin from scratch and show you how to read custom values injected into **AndroidManifest.xml** and **Info.plist** by your plugin from an Expo module.
15
16## 1. Initialize a module
17
18Start by initializing a new Expo module project using `create-expo-module`, which will provide scaffolding for Android, iOS, and TypeScript. It will also provide an example project to interact with the module from within an app. Run the following command to initialize it:
19
20<Terminal cmd={['$ npx create-expo-module expo-native-configuration']} />
21
22We will use the name `expo-native-configuration`/`ExpoNativeConfiguration` for the project. You can name it whatever you like.
23
24## 2. Set up our workspace
25
26In our example, we won't need the view module included by `create-expo-module`. Let's clean up the default module a little bit with the following command:
27
28<Terminal
29  cmdCopy="cd expo-native-configuration && rm ios/ExpoNativeConfigurationView.swift && rm android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationView.kt && rm src/ExpoNativeConfigurationView.tsx src/ExpoNativeConfiguration.types.ts && rm src/ExpoNativeConfigurationView.web.tsx src/ExpoNativeConfigurationModule.web.ts"
30  cmd={[
31    '$ cd expo-native-configuration',
32    '$ rm ios/ExpoNativeConfigurationView.swift',
33    '$ rm android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationView.kt',
34    '$ rm src/ExpoNativeConfigurationView.tsx src/ExpoNativeConfiguration.types.ts',
35    '$ rm src/ExpoNativeConfigurationView.web.tsx src/ExpoNativeConfigurationModule.web.ts',
36  ]}
37/>
38
39We also need to find **ExpoNativeConfigurationModule.swift**, **ExpoNativeConfigurationModule.kt**, **src/index.ts** and **example/App.tsx** and replace them with the provided minimal boilerplate:
40
41```swift ios/ExpoNativeConfigurationModule.swift
42import ExpoModulesCore
43
44public class ExpoNativeConfigurationModule: Module {
45  public func definition() -> ModuleDefinition {
46    Name("ExpoNativeConfiguration")
47
48    Function("getApiKey") { () -> String in
49      "api-key"
50    }
51  }
52}
53```
54
55```kotlin android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt
56package expo.modules.nativeconfiguration
57
58import expo.modules.kotlin.modules.Module
59import expo.modules.kotlin.modules.ModuleDefinition
60
61class ExpoNativeConfigurationModule : Module() {
62  override fun definition() = ModuleDefinition {
63    Name("ExpoNativeConfiguration")
64
65    Function("getApiKey") {
66      return@Function "api-key"
67    }
68  }
69}
70```
71
72```typescript src/index.ts
73import ExpoNativeConfigurationModule from './ExpoNativeConfigurationModule';
74
75export function getApiKey(): string {
76  return ExpoNativeConfigurationModule.getApiKey();
77}
78```
79
80```typescript example/App.tsx
81import * as ExpoNativeConfiguration from 'expo-native-configuration';
82import { Text, View } from 'react-native';
83
84export default function App() {
85  return (
86    <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
87      <Text>API key: {ExpoNativeConfiguration.getApiKey()}</Text>
88    </View>
89  );
90}
91```
92
93## 3. Run the example project
94
95Now let's run the example project to make sure everything is working. Start the TypeScript compiler to watch for changes and rebuild the module JavaScript.
96
97<Terminal
98  cmdCopy="npm run build"
99  cmd={[
100    '# Run this in the root of the project to start the TypeScript compiler',
101    '$ npm run build',
102  ]}
103/>
104
105In another terminal window, compile and run the example app:
106
107<Terminal
108  cmdCopy="cd example && npx expo run:ios"
109  cmd={[
110    '$ cd example',
111    '# Run the example app on iOS',
112    '$ npx expo run:ios',
113    '# Run the example app on Android',
114    '$ npx expo run:android',
115  ]}
116/>
117
118We should see a screen with a text saying `API key: api-key`. Now let's develop the plugin to inject our custom API key.
119
120## 4. Creating a new config plugin
121
122Let's start developing our new config plugin. Plugins are synchronous functions that accept an `ExpoConfig` and return a modified `ExpoConfig`. By convention, these functions are prefixed by the word `with`. We will name our plugin `withMyApiKey`. Feel free to call it whatever you like, as long as it follows the convention.
123
124Here is an example of how a basic config plugin function looks:
125
126```javascript
127const withMyApiKey = config => {
128  return config;
129};
130```
131
132Additionally, you can use `mods`, which are async functions that modify files in native projects such as source code or configuration (plist, xml) files. The `mods` object is different from the rest of the Expo config because it doesn't get serialized after the initial reading. This means you can use it to perform actions *during* code generation.
133
134However, there are a few considerations that we should follow when writing config plugins:
135
136- Plugins should be synchronous and their return value should be serializable, except for any `mods` that are added.
137- `plugins` are always invoked when the config is read by the `expo/config` method `getConfig`. However, `mods` are only invoked during the "syncing" phase of `npx expo prebuild`.
138
139> Although not required, we can use [`expo-module-scripts`](https://www.npmjs.com/package/expo-module-scripts) to make plugin development easier — it provides a recommended default configuration for TypeScript and Jest. For more information, see [config plugins guide](https://github.com/expo/expo/tree/main/packages/expo-module-scripts#-config-plugin).
140
141Let's start by creating our plugin with this minimal boilerplate. This will create a **plugin** folder where we will write the plugin in TypeScript and add a **app.plugin.js** file in the project root, which will be the entry file for the plugin.
142
1431. Create a **plugin/tsconfig.json** file:
144
145   ```json plugin/tsconfig.json
146   {
147     "extends": "expo-module-scripts/tsconfig.plugin",
148     "compilerOptions": {
149       "outDir": "build",
150       "rootDir": "src"
151     },
152     "include": ["./src"],
153     "exclude": ["**/__mocks__/*", "**/__tests__/*"]
154   }
155   ```
156
1572. Create a **plugin/src/index.ts** file for our plugin:
158
159   ```typescript plugin/src/index.ts
160   import { ConfigPlugin } from 'expo/config-plugins';
161
162   const withMyApiKey: ConfigPlugin = config => {
163     console.log('my custom plugin');
164     return config;
165   };
166
167   export default withMyApiKey;
168   ```
169
1703. Finally, create an **app.plugin.js** file in the root directory. That will configure the entry file for our plugin:
171
172   ```javascript app.plugin.js
173   module.exports = require('./plugin/build');
174   ```
175
176At the root of your project, run `npm run build plugin` to start the TypeScript compiler in watch mode. The only thing left to configure is our example project to use our plugin. We can achieve this by adding the following line to the **example/app.json** file.
177
178```json example/app.json
179{
180  "expo": {
181    ...
182    "plugins": ["../app.plugin.js"]
183  }
184}
185```
186
187Now when running `npx expo prebuild` inside our **example** folder we should see our 'my custom plugin’ console.log statement in the terminal.
188
189<Terminal cmd={['$ cd example', '$ npx expo prebuild --clean']} />
190
191To inject our custom API keys into **AndroidManifest.xml** and **Info.plist** we can use a few helper [`mods` provided by `expo/config-plugins`](/config-plugins/plugins-and-mods/#what-are-mods), which makes it easy to modify native files. In our example, we will use two of them, `withAndroidManifest` and `withInfoPlist`.
192
193As the name suggests, `withInfoPlist` allows us to read and modify **Info.plist** values. Using the `modResults` property, we can add custom values as demonstrated in the code snippet below:
194
195```typescript
196const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
197  config = withInfoPlist(config, config => {
198    config.modResults['MY_CUSTOM_API_KEY'] = apiKey;
199    return config;
200  });
201
202  return config;
203};
204```
205
206Similarly, we can use `withAndroidManifest` to modify the **AndroidManifest.xml** file. In this case, we will utilize `AndroidConfig` helpers to add a meta data item to the main application:
207
208```typescript
209const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
210  config = withAndroidManifest(config, config => {
211    const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
212
213    AndroidConfig.Manifest.addMetaDataItemToMainApplication(
214      mainApplication,
215      'MY_CUSTOM_API_KEY',
216      apiKey
217    );
218    return config;
219  });
220
221  return config;
222};
223```
224
225We can create our custom plugin by merging everything into a single function:
226
227```typescript plugin/src/index.ts
228import {
229  withInfoPlist,
230  withAndroidManifest,
231  AndroidConfig,
232  ConfigPlugin,
233} from 'expo/config-plugins';
234
235const withMyApiKey: ConfigPlugin<{ apiKey: string }> = (config, { apiKey }) => {
236  config = withInfoPlist(config, config => {
237    config.modResults['MY_CUSTOM_API_KEY'] = apiKey;
238    return config;
239  });
240
241  config = withAndroidManifest(config, config => {
242    const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
243
244    AndroidConfig.Manifest.addMetaDataItemToMainApplication(
245      mainApplication,
246      'MY_CUSTOM_API_KEY',
247      apiKey
248    );
249    return config;
250  });
251
252  return config;
253};
254
255export default withMyApiKey;
256```
257
258Now with the plugin ready to be used, let's update the example app to pass our API key to the plugin as a configuration option. Modify the `"plugins"` field in **example/app.json** as shown below:
259
260```json example/app.json
261{
262  "expo": {
263    ...
264    "plugins": [["../app.plugin.js", { "apiKey": "custom_secret_api" }]]
265  }
266}
267```
268
269After making this change, we can test that the plugin is working correctly by running the command `npx expo prebuild --clean` inside the **example** folder. This will execute our plugin and update native files, injecting "MY_CUSTOM_API_KEY" into **AndroidManifest.xml** and **Info.plist**. You can verify this by checking the contents of **example/android/app/src/main/AndroidManifest.xml**.
270
271## 5. Reading native values from the module
272
273Now let's make our native module read the fields we added to **AndroidManifest.xml** and **Info.plist**. This can be done by using platform-specific methods to access the contents of these files.
274
275On iOS, we can read the content of an **Info.plist** property by using the `Bundle.main.object(forInfoDictionaryKey:  "")` instance Method. To read the `"MY_CUSTOM_API_KEY"` value that we added earlier, update the **ios/ExpoNativeConfigurationModule.swift** file:
276
277```swift ios/ExpoNativeConfigurationModule.swift
278import ExpoModulesCore
279
280public class ExpoNativeConfigurationModule: Module {
281  public func definition() -> ModuleDefinition {
282    Name("ExpoNativeConfiguration")
283
284    Function("getApiKey") {
285     return Bundle.main.object(forInfoDictionaryKey: "MY_CUSTOM_API_KEY") as? String
286    }
287  }
288}
289```
290
291On Android, we can access metadata information from the **AndroidManifest.xml** file using the `packageManager` class. To read the `"MY_CUSTOM_API_KEY"` value, update the **android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt** file:
292
293```kotlin android/src/main/java/expo/modules/nativeconfiguration/ExpoNativeConfigurationModule.kt
294package expo.modules.nativeconfiguration
295
296import expo.modules.kotlin.modules.Module
297import expo.modules.kotlin.modules.ModuleDefinition
298import android.content.pm.PackageManager
299
300class ExpoNativeConfigurationModule() : Module() {
301  override fun definition() = ModuleDefinition {
302    Name("ExpoNativeConfiguration")
303
304    Function("getApiKey") {
305      val applicationInfo = appContext?.reactContext?.packageManager?.getApplicationInfo(appContext?.reactContext?.packageName.toString(), PackageManager.GET_META_DATA)
306
307      return@Function applicationInfo?.metaData?.getString("MY_CUSTOM_API_KEY")
308    }
309  }
310}
311```
312
313## 6. Running your module
314
315With our native modules reading the fields we added to the native files, we can now run the example app and access our custom API key through the `ExamplePlugin.getApiKey()` function.
316
317<Terminal
318  cmdCopy="cd example && npx expo run:ios"
319  cmd={[
320    '$ cd example',
321    '# execute our plugin and update native files',
322    '$ npx expo prebuild',
323    '# Run the example app on iOS',
324    '$ npx expo run:ios',
325    '# Run the example app on Android',
326    '$ npx expo run:android',
327  ]}
328/>
329
330## Next steps
331
332Congratulations, you have created a simple yet non-trivial config plugin that interacts with an Expo module for Android and iOS!
333
334If you want to challenge yourself and make the plugin more versatile we leave this exercise open to you. Try modifying the plugin to allow for any arbitrary set of config keys/values to be passed in and adding the functionality to allow for the reading of arbitrary keys from the module.
335